Applications Azure

AirTrail on Ubuntu 24.04 on Azure User Guide

| Product: AirTrail on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment and configuration of AirTrail on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. AirTrail is an open source, self-hosted flight tracker and travel log. You record the flights you take, with route, airline, aircraft and seat, and AirTrail draws each journey as a great-circle arc on an interactive world map so your travel history builds into a picture you can explore. Alongside the map it keeps running statistics for distance flown, time in the air, airports and countries visited, airlines and aircraft types. Flights can be added by hand, looked up from a flight number, or imported in bulk, and several people can share one instance, each with their own account and their own map.

The cloudimg image ships the free and open source, GPL-3.0 licensed AirTrail application, run the officially supported way as the upstream container pinned by image digest, with PostgreSQL 16 as its database. Both images are captured into the VM, so your instance starts in seconds. Upstream AirTrail makes the first visitor to a new instance its owner, which is a real risk for a VM with a public IP; this image closes that window completely. On first boot, before the public port is ever opened, the instance generates a per-VM PostgreSQL password and claims the owner account itself with a per-VM random password, written to a root-only file. Backed by 24/7 cloudimg support.

AirTrail is a trademark of its respective owner. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by the AirTrail project. It ships the free and open source self-hosted software, unmodified.

The docker, airtrail-firstboot, airtrail, airtrail-postboot and nginx services all active, and both compose containers healthy - the AirTrail app published only to 127.0.0.1:3000 and PostgreSQL on the internal container network with no host port at all

What is included:

  • AirTrail v3.11.1 (the GPL-3.0 licensed SvelteKit flight tracker), pinned by image digest
  • PostgreSQL 16, created empty on first boot and stored on a persistent volume, reachable only from the application container
  • Docker Engine running both containers on a private network, with the app published to loopback only
  • Host nginx as the single public listener on port 80, with header buffers sized for authenticated responses
  • airtrail.service, airtrail-firstboot.service and airtrail-postboot.service as systemd units, enabled and active on boot
  • A per-VM PostgreSQL password and a per-VM owner account password, generated on first boot and never baked into the image
  • The owner account claimed during bootstrap while port 80 is still closed, so a passer-by can never claim the instance
  • No open self-registration: further accounts are created by the owner from Settings
  • No default login, no shipped secret, no bundled third-party API key and an empty database on first boot
  • Ubuntu 24.04 LTS base with latest security patches applied at build time
  • Azure Linux Agent for seamless cloud integration and SSH key injection
  • 24/7 cloudimg support with guaranteed 24 hour response SLA

Prerequisites

  • Active Azure subscription, SSH public key, VNet + subnet in target region
  • Subscription to the AirTrail listing on Azure Marketplace

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) comfortably exceeds AirTrail's stated minimum of 2 CPU cores, 2 GB RAM and 10 GB disk, and is a sensible starting point for a person, a household or a small team. For many concurrent users use Standard_D2s_v5 or larger. NSG inbound: allow 22/tcp from your management network and 80/tcp for the web app from the networks that use it.

Step 1: Deploy from the Azure Portal

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for AirTrail by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + create and Create.

Step 2: Deploy from the Azure CLI

RG="airtrail-prod"; LOCATION="eastus"; VM_NAME="airtrail-01"
GALLERY_IMAGE_ID="/subscriptions/<sub-id>/resourceGroups/azure-cloudimg/providers/Microsoft.Compute/galleries/cloudimgGallery/images/airtrail-ubuntu-24-04/versions/<version>"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
az network vnet create -g "$RG" --name airtrail-vnet --address-prefix 10.100.0.0/16 --subnet-name airtrail-subnet --subnet-prefix 10.100.1.0/24
az network nsg create -g "$RG" --name airtrail-nsg
az network nsg rule create -g "$RG" --nsg-name airtrail-nsg --name allow-ssh --priority 100 \
  --source-address-prefixes "<your-mgmt-cidr>" --destination-port-ranges 22 --access Allow --protocol Tcp
az network nsg rule create -g "$RG" --nsg-name airtrail-nsg --name allow-http --priority 110 \
  --destination-port-ranges 80 --access Allow --protocol Tcp
az vm create -g "$RG" --name "$VM_NAME" --image "$GALLERY_IMAGE_ID" \
  --size Standard_B2s --storage-sku StandardSSD_LRS \
  --admin-username azureuser --ssh-key-values "$SSH_KEY" \
  --vnet-name airtrail-vnet --subnet airtrail-subnet --nsg airtrail-nsg --public-ip-sku Standard

Replace <sub-id> and <version> with your subscription id and the image version shown on the listing, and <your-mgmt-cidr> with the network you administer from.

Step 3: First boot and per-instance credentials

On the first boot of each VM, AirTrail generates its own per-instance secrets and claims its own owner account before the web port is opened. Nothing is baked into the image. Give the instance a minute after first boot, then SSH in and read the credentials file (it is 0600 root:root):

sudo cat /root/airtrail-credentials.txt

The per instance credentials file, showing the AirTrail owner login and the PostgreSQL password each generated randomly for this VM (masked here) and the resolved web URL, with both the credentials file and the environment file confirmed as 0600 root:root

The file contains everything unique to this instance:

  • AIRTRAIL_USERNAME / AIRTRAIL_PASSWORD — the owner login you use in the web UI. The owner has full access, including user management.
  • AIRTRAIL_URL — the web address of this instance, resolved from its public IP on first boot.
  • POSTGRES_PASSWORD — the database role password. PostgreSQL is not published to any host port, so this is only used inside the private container network.

Confirm the services and containers are healthy:

sudo systemctl is-active docker airtrail-firstboot airtrail airtrail-postboot nginx
active
active
active
active
active
sudo docker compose -f /opt/airtrail/docker-compose.yml ps --format 'table {{.Service}}\t{{.Status}}\t{{.Ports}}'
SERVICE    STATUS                   PORTS
airtrail   Up 8 minutes (healthy)   127.0.0.1:3000->3000/tcp
db         Up 8 minutes (healthy)   5432/tcp

The application container publishes to loopback only and PostgreSQL has no host port at all; host nginx on port 80 is the single public door.

Step 4: The security model

Upstream AirTrail guides the first person who opens a new instance through creating the owner account. On a VM with a public IP that is a real risk, so cloudimg removes it in two independent ways.

First, airtrail-postboot.service claims the owner account itself, over the loopback port, with a password generated for this VM. AirTrail's own setup endpoint then refuses forever with Owner already exists, which is enforced by the application's code rather than by a configuration flag. Second, host nginx — the only public listener — does not start until that has happened: it is gated on a bootstrap-ready marker that post-boot creates only after the owner exists. Port 80 is therefore closed for the whole bootstrap window.

The database password is generated on first boot too, before the stack can start, so the upstream sample value (DB_PASSWORD=password) never exists on your VM. A start-up guard refuses to launch the application at all if that password is empty, too short or a known default.

The security posture: the AirTrail app listening only on 127.0.0.1:3000, nginx the only 0.0.0.0:80 listener and no PostgreSQL host port, with a form POST carrying no Origin header refused 403, a second owner setup refused with Owner already exists, and the authenticated-only map API returning 401 anonymously

ss -tln | grep -E ':(80|3000|5432) ' | awk '{print $1, $4}'
LISTEN 127.0.0.1:3000
LISTEN 0.0.0.0:80
LISTEN [::]:80

Cross-site form submissions are rejected, and map data that belongs to a signed-in user is not readable anonymously:

curl -s -o /dev/null -w 'form POST without an Origin header -> %{http_code}\n' -X POST http://127.0.0.1/api/users/setup --data-urlencode username=probe --data-urlencode password=probeprobe1 --data-urlencode displayName=probe
curl -s -o /dev/null -w 'authenticated-only map API (anonymous) -> %{http_code}\n' http://127.0.0.1/api/map-styles/openaip/tiles/1/1/1
form POST without an Origin header -> 403
authenticated-only map API (anonymous) -> 401

Exactly one account exists on a fresh instance, and it is the owner:

sudo docker exec airtrail-db psql -w -U airtrail -d airtrail -tAc "SELECT username, role FROM \"user\"" </dev/null
admin|owner

Step 5: Verify the end-to-end round-trip

The image ships a prover that exercises the whole security model through the public front door: it confirms the app answers, that anonymous visitors are sent to the login page, that cross-site form posts are refused, that the owner seat is taken, that a wrong password is rejected, and that the per-instance password logs in and reads back real authenticated data.

sudo /usr/local/sbin/airtrail-roundtrip.sh

The credential round-trip prover reporting OK: ping returned pong, anonymous page data redirects to login, a cross-site form POST was refused 403, the owner seat is taken, a wrong password was rejected, the authenticated-only API moved from 401 to 503 once a session existed, login returned 303 and the authenticated data readback carries the owner

Step 6: Sign in to the web app

Open http://<vm-ip>/ in your browser and sign in with the AIRTRAIL_USERNAME and AIRTRAIL_PASSWORD from the credentials file.

The AirTrail login page asking for a username and password, for the owner account created on this VM during first boot

Once signed in, the map is the home view. Every flight you log is drawn as a great-circle arc between its two airports, so your travel history accumulates into a picture of where you have been. The controls on the right zoom, re-centre, switch the base map and filter which flights are shown; the dock along the bottom opens the flight list, statistics and settings.

The signed-in AirTrail world map with four logged flights drawn as great-circle arcs between their airports - London to New York, New York to Los Angeles, London to Singapore and San Francisco to Tokyo - proving the flight database is live and the route map renders

Choose Statistics from the dock for the totals AirTrail derives from your logged flights: number of flights, distance flown expressed as a fraction of the way around the globe, total time in the air, and how many airports and countries you have visited, plus breakdowns by seat class, cabin, trip reason, continent and airline.

The AirTrail statistics view showing 4 flights, 28,624 km flown which is 0.71 times around the globe, 1 day 14 hours of flight time and 6 airports visited, with charts breaking the flights down by seat class, seat preference, trip reason, continent and top airlines

List flights shows every flight in date order with its airline, flight number, route, duration and seat. Use Add flight in the dock to record a new one — you can enter it by hand or look it up by flight number and date.

The AirTrail All Flights list showing four logged flights with airline logos, flight numbers, origin and destination airports and durations - San Francisco to Tokyo 10h 50m, London to Singapore 13h 5m, New York to Los Angeles 6h 25m and London to New York 7h 40m

Adding other people

There is no open self-registration on this image, so nobody can create an account on your instance. To add a family member or colleague, sign in as the owner, open Settings from the dock, choose Users, and add the account there. Each person gets their own flights, their own map and their own statistics.

Map data and optional integrations

The base map is served by CARTO's public basemap CDN, and airport locations come from a map data file baked into the image. Neither needs an API key, and no map key is shipped or required — but the base map tiles are fetched by the browser from the public internet, so a client with no outbound internet access will see the flight arcs draw over an empty background rather than a map.

Two optional integrations are left deliberately unset, and both are yours to supply if you want them:

  • OpenAIP — adds an aeronautical overlay (airspaces, navaids, reporting points). Without a key that overlay simply returns "not configured" and the rest of the map is unaffected.
  • AeroDataBox — enriches flight lookup by flight number.

Add either from Settings in the web UI. No third-party key is baked into this image.

Using your own domain

On first boot the instance sets its own address, ORIGIN, to http://<vm-ip>. AirTrail uses that value to decide which form submissions to trust, so if you reach the VM by a DNS name rather than its IP address you must update it. Point a hostname at the VM, then set ORIGIN in /opt/airtrail/.env to that URL — for example https://airtrail.example.com if you terminate TLS on a reverse proxy in front — and restart the service:

sudo systemctl restart airtrail

The same applies if the VM's public IP changes, for example after a deallocate and start.

Managing the service

# view recent application logs
sudo docker compose -f /opt/airtrail/docker-compose.yml logs --tail 50 airtrail
# restart the whole stack
sudo systemctl restart airtrail

The upstream release notes and complete corresponding source for the version shipped here are recorded on the VM at /usr/share/doc/airtrail/, alongside the GPL-3.0 licence text.

Support

cloudimg provides 24/7 support with a guaranteed 24 hour response SLA. Contact support@cloudimg.co.uk for assistance with this image.