Self-Hosting with Headscale

Aircast reaches your device over a tailnet — a private WireGuard mesh coordinated by a control server. By default that's Tailscale's hosted coordination service. If you'd rather own the whole path, point the device at your own Headscale server instead. No Tailscale account, no third-party coordinator — just your control plane and your devices.

Before you start

  • A small always-on host with a public IP or DNS name for Headscale (a $5 VPS is plenty — it carries control traffic, not video).
  • A domain with HTTPS for the control server. Headscale clients require TLS; terminate it with Caddy, a reverse proxy, or Headscale's built-in ACME.
  • An Aircast device already flashed and reachable on your LAN — see Aircast.

Throughout this guide, replace https://headscale.example.com with your own control server URL.

Set it up

1

Deploy Headscale

Headscale reads its config at startup, so create the data and config directories first. Grab the example config and set server_url to your public URL:

On your control-server host
mkdir -p ./config ./data
curl -fsSL https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml \
  -o ./config/config.yaml
# edit ./config/config.yaml → set: server_url: https://headscale.example.com

Then start the container, mounting both directories:

On your control-server host
docker run -d --name headscale \
  -v ./config:/etc/headscale \
  -v ./data:/var/lib/headscale \
  -p 8080:8080 \
  headscale/headscale:stable \
  serve

Front it with HTTPS so it's reachable at https://headscale.example.com — Headscale clients require TLS. Verify it's up:

On your control-server host
curl -fsS https://headscale.example.com/health

See the Headscale documentation for the full config reference and reverse-proxy examples.

2

Create a user

Devices register under a user (a namespace). Create one — the name is yours to pick:

On your control-server host
headscale users create aircast

If you run Headscale in Docker, prefix these with docker exec headscale, e.g. docker exec headscale headscale users create aircast.

3

Mint a pre-auth key

A pre-auth key lets the device join unattended — no browser sign-in, no manual approval. This is the path Aircast is built around. Generate one for your user:

On your control-server host
headscale preauthkeys create --user aircast --expiration 24h

It prints a long key. Copy it — you'll paste it into the device in the next step.

4

Point the device at your server

Open the device dashboard on your LAN (http://<hostname>.local) and find the Tailscale section. Expand the control-server settings and enter:

  • Control server — your URL, e.g. https://headscale.example.com
  • Auth key — the pre-auth key from step 3

Save, then Connect (or Switch, if the device is already on another tailnet). The device registers itself against your Headscale and appears under the aircast user within a few seconds — no approval step.

5

Confirm it joined

On your control-server host, list the registered nodes:

On your control-server host
headscale nodes list

Your device shows up with an assigned tailnet IP. The dashboard's Tailscale section now reads Connected with its tailnet name. You can reach the device at that address from anywhere on the same tailnet.

Manage it from a browser

Everything above works from the CLI, but a web UI like headscale-admin is handy for day-to-day node and user management. Point it at your server: open its Settings, set the API URL to your control server, and paste an API key from headscale apikeys create. Once it reads Authorized, you're connected.

headscale-admin Settings page with the API URL and API key filled in, showing an Authorized status.
headscale-admin → Settings: enter your control server URL and an API key, then save.

The Nodes tab then lists every registered device with its online status — your Aircast device shows up here alongside any phones or laptops on the tailnet.

headscale-admin Nodes tab listing registered devices with online indicators.
The Nodes tab — registered devices and their live status.

Join your viewing devices

To watch the stream, the device you view from — a phone or laptop — also needs to be on your tailnet. In the official Tailscale app you can point at a self-hosted Headscale instead of Tailscale's servers: add an account with Use an alternate server (enter your URL) or Use an auth key (paste a pre-auth key).

On a phone, open the account menu and pick one of the two options:

Tailscale app account menu with 'Use an alternate server' and 'Use an auth key' options.
Account menu → alternate server or auth key.
Tailscale app 'Use an alternate server' screen with a custom control server URL field.
Enter your Headscale URL as the custom control server.
Tailscale app 'Add an account using an auth key' screen with an auth key field.
Or paste a pre-auth key to join unattended.

On macOS or desktop it's the same — Add Account → Use an alternate server, then enter your Headscale URL:

Tailscale macOS app 'Add Account Using Alternate Server' dialog with a custom server URL field.
Tailscale on macOS → Add Account → Use an alternate server.

Once it connects, the account's Server shows your Headscale URL — confirmation the device is on your control plane, not Tailscale's:

Tailscale macOS app Accounts tab showing a logged-in account whose Server is the self-hosted Headscale.
Connected: the account is signed in to your self-hosted Headscale.

On the CLI it's tailscale up --login-server https://headscale.example.com (interactive) or the same with --authkey (unattended).

Connect QGroundControl over the tailnet

With both ends on the tailnet, your ground station reaches the device by its tailnet address — no port forwarding, no public exposure. In QGroundControl, wire up telemetry and video to the device's tailnet IP/hostname.

Telemetry (MAVLink)

Open Application Settings → Comm Links → Add. Create a UDP link and add a server address pointing at the device's tailnet IP on the MAVLink port — typically <device-tailnet-ip>:14550:

QGroundControl Edit Link dialog: a UDP link named Headscale with server address 100.64.0.1:14550.
A UDP comm link pointed at the device's tailnet IP, port 14550.

Save it, then Connect — it sits alongside any other links (e.g. a direct 4G link) so you can switch between paths:

QGroundControl Comm Links list showing a 4g link and a Headscale link, each with a Disconnect button.
The tailnet link lives next to your other comm links.

Video (RTSP)

Under Application Settings → Video, set the source to RTSP Video Stream and the URL to the device over the tailnet: rtsp://<device-tailnet-host>:8554/cam. A second source lets you toggle between cameras.

QGroundControl Video settings with RTSP Video Stream sources and RTSP URLs over the tailnet.
RTSP video pointed at the device's tailnet hostname, port 8554.

Troubleshooting: “key hex string doesn't have expected type prefix mkey:”

headscale-admin Create node form with a bare hex key pasted (no mkey prefix), which is rejected.
Pasting only the hex digits — no mkey: prefix — triggers the error.

Where does the key come from? A node can't be created from a hostname — it only exists once the device starts registration, which is what generates its machine key. When a client signs in to a self-hosted Headscale interactively, it shows a Machine registration page with a ready-to-run command. The value after --key — including the mkey: prefix — is exactly what the form expects.

A device's Machine registration page showing 'headscale nodes register --user USERNAME --key mkey:…'.
The device's sign-in page hands you the mkey: key (and the matching register command).

Paste that full mkey:-prefixed value into the Create form, pick the user, and submit — the node registers cleanly.

headscale-admin Create node form with the full mkey-prefixed key and a user selected.
With the mkey: prefix and a user selected, the key is accepted.
headscale-admin showing a newly created node with its details and a success toast.
The node is created and appears in the list.