On this page

    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 and 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, since 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 First flight. Or set Headscale while you flash the card, see step 5.

    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

    Connect the admin panel

    Everything from here on happens in headscale-admin , a web UI for users, pre-auth keys, and nodes. Run it next to Headscale and serve it from the same domain under /admin:

    On your control-server host
    docker run -d --name headscale-admin \
      -p 8000:80 \
      goodieshq/headscale-admin:latest
    # reverse-proxy https://headscale.example.com/admin → localhost:8000

    The panel authenticates against Headscale with an API key, so mint one:

    On your control-server host
    docker exec headscale headscale apikeys create --expiration 90d

    Open https://headscale.example.com/admin → Settings, set the API URL to your control server, paste the API key, and save. Once it reads Authorized, you're connected. This is the last time you touch the terminal.

    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.
    3

    Create a user

    Devices register under a user (a namespace). In the Users tab, click Create and name it. The user's card then lists every node it owns and its pre-auth keys:

    headscale-admin Users tab with a user card expanded, showing its nodes and a PreAuth Keys section with a Create button.
    The user card, with its nodes and the PreAuth Keys section you'll use next.
    4

    Mint a pre-auth key

    A pre-auth key lets the device join unattended, with no browser sign-in and no manual approval. This is the path Aircast is built around. On the user's card, next to PreAuth Keys, click Create. Copy the hskey-… value, which you'll paste into the device in the next step.

    headscale-admin PreAuth Keys section listing minted hskey keys with Used, Expired, Ephemeral, and Reusable badges.
    Minted keys live on the user card, with their Reusable/Ephemeral state at a glance.
    5

    Point the device at your server

    Open the device on your LAN (http://<hostname>.local), go to Settings → Device → Connection, and find the Tailscale card. Expand Control server and enter:

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

    Press Save, then Connect to headscale.example.com (or Switch to headscale.example.com, if the device is already on another tailnet). The device registers itself against your Headscale and appears under the key's user within a few seconds, with no approval step.

    Flashing a new card? In the Flasher's Remote access section, pick Headscale, enter the Control server URL, and paste the pre-auth key (or a Headscale API key, then pick the user). The drone joins your tailnet on first boot.

    6

    Confirm it joined

    The admin panel's Nodes tab lists every registered device with its online status and tailnet IP. Your Aircast device shows up here alongside any phones or laptops on the tailnet. The device's Tailscale card now reads connected with its tailnet name, and you can reach it at that address from anywhere on the same tailnet.

    headscale-admin Nodes tab listing registered devices with online indicators.
    The Nodes tab, showing 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, add an account with Use an alternate server and enter your Headscale URL, and the app signs in against your control plane instead of Tailscale's.

    On a phone, open the account menu and pick Use an alternate server:

    Tailscale app account menu with the 'Use an alternate server' option.
    Account menu → Use an alternate server.
    Tailscale app 'Use an alternate server' screen with a custom control server URL field.
    Enter your Headscale URL as the custom control server.

    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, confirming 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.

    Joining unattended (CLI, or a machine without a browser)? tailscale up --login-server https://headscale.example.com --authkey <pre-auth key>, the app's Use an auth key option does the same.

    Tailscale mobile app 'Add an account using an auth key' screen with an auth key field.
    The app's Use an auth key screen. Paste the Headscale pre-auth key you minted in step 4, not a Tailscale one.

    Connect QGroundControl over the tailnet

    With both ends on the tailnet, your ground station reaches the device by its tailnet address, with no port forwarding, no public exposure. In Aircast QGC, point telemetry at the device's tailnet IP or name, and video follows.

    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)

    Once telemetry arrives over this link, the device announces its camera and Aircast QGC sets up Camera 1 by itself. For another camera, or a ground station that doesn't pick it up, open Application Settings → Video, press Add camera stream under Cameras, and use the device's tailnet name: rtsp://<device-tailnet-host>:8554/<camera name>.

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

    If you do register a node through headscale-admin's Create form, you'll hit this error when you paste a key without its mkey: prefix. The field wants the full prefixed key, not the bare hex.

    headscale-admin Create node form with a bare hex key pasted (no mkey prefix), which is rejected.
    Pasting only the hex digits, without the 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.

    Flash your card in about 5 minutes

    The Aircast Flasher downloads the image, sets Wi-Fi and hostname, and verifies the card.

    Download the Flasher