Blog / Tutorials

Self-Host Headscale — Run Your Own Tailscale Control Plane

By the NoctHost TeamSeptember 2, 20267 min read

Tailscale feels like magic: install the client on every machine, log in, and they can all reach each other over WireGuard without you touching a single firewall rule or NAT traversal setting. The magic comes from a coordination server that Tailscale runs. It holds every device's public key, hands out addresses, distributes the ACLs, and brokers the initial connection between two peers.

Your traffic never flows through that server — WireGuard is peer-to-peer. But the server knows the shape of your entire network, and it decides who can talk to whom. Headscale is an open-source reimplementation of exactly that control plane, so you can run it yourself and keep the coordination keys in your own hands.

What Headscale actually replaces

The Tailscale system has two halves, and it is worth being precise about which one Headscale swaps out:

  • The data plane — the encrypted WireGuard tunnels between your machines. This is the real Tailscale client (tailscale / tailscaled), and Headscale does not touch it. You keep using the official clients on every device.
  • The control plane — the coordination server at login.tailscale.com that authenticates devices and tells them about each other. This is what Headscale becomes.

So the honest one-line summary: Headscale lets you point the official Tailscale clients at your own server instead of Tailscale's. You still get MagicDNS, subnet routers, exit nodes, and NAT traversal. You just stop trusting a third party to coordinate them.

Who this is actually for

Most people running Tailscale should keep running hosted Tailscale. It is free for personal use, the DERP relay network is genuinely good, and the coordination server never sees your traffic anyway. Self-hosting the control plane is more moving parts for a benefit that is real but narrow.

You want Headscale if one of these is true:

  • You have a policy or philosophical reason not to depend on a third party for network membership — you do not want anyone else able to add a node or read your network topology.
  • You are past the free tier's device or user limits and would rather run a server than pay per seat.
  • You want the control plane on infrastructure you fully own for compliance or air-gap-adjacent reasons.

If none of those bite, hosted Tailscale is the saner choice and you should close this tab guilt-free.

The honest tradeoffs

Headscale is a community project, not the commercial product with the polish stripped out. Going in, expect:

  • No admin GUI. Configuration is a YAML file and a CLI. There is no web dashboard in the box (third-party ones exist, and they are extra services you now also run and secure). If you like clicking on things, this will annoy you.
  • You run the auth. Hosted Tailscale wires up Google/GitHub/Okta SSO for you. Headscale supports OIDC, but you configure the identity provider yourself, or you fall back to pre-auth keys and register nodes by hand.
  • You are now on call for it. If the Headscale server goes down, existing tunnels keep working (they are peer-to-peer), but you cannot add nodes, keys cannot re-issue, and expiring nodes drop off. It is a real service you have to keep alive and patched.
  • Feature lag. Headscale tracks the Tailscale protocol closely but not instantly. Occasionally a shiny new client feature lands before Headscale supports it.

None of this is a dealbreaker. It is just the actual shape of what you are signing up for.

Setting it up

You need a VPS with a public IP and a domain pointing at it — the clients connect to Headscale over HTTPS, and a real certificate keeps that simple. Ubuntu 24.04 assumed.

Run Headscale and Caddy (for automatic TLS) with Docker Compose. Save as docker-compose.yml:

services:
  headscale:
    image: headscale/headscale:0.23.0
    restart: always
    command: serve
    volumes:
      - ./config:/etc/headscale
      - ./data:/var/lib/headscale
    ports:
      - "127.0.0.1:8080:8080"

  caddy:
    image: caddy:2-alpine
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
    depends_on:
      - headscale

volumes:
  caddy_data:

Pin a real version tag rather than latest — Headscale has occasionally shipped breaking config changes between minor versions, and you do not want an unattended pull to strand your network.

The Caddyfile next to it, which terminates HTTPS and proxies to Headscale:

headscale.example.com {
  reverse_proxy headscale:8080
}

Now the config. Create config/config.yaml. The fields that actually matter:

server_url: https://headscale.example.com
listen_addr: 0.0.0.0:8080
prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48
dns:
  base_domain: internal.example.com
  magic_dns: true
  nameservers:
    global:
      - 1.1.1.1

Two things people get wrong here. First, server_url must be the public HTTPS URL exactly as the clients will reach it — a mismatch produces a maddening "TLS handshake" or "unexpected 404" on tailscale up. Second, base_domain must not overlap with any real domain your machines resolve, or MagicDNS names will collide with public records.

Bring it up:

docker compose up -d

Registering the first node

Headscale groups devices under a user (namespace). Create one, then generate a pre-auth key:

docker compose exec headscale headscale users create alice
docker compose exec headscale headscale preauthkeys create --user alice --expiration 1h

That prints a key. On the machine you want to add, install the normal Tailscale client, then point it at your server instead of Tailscale's:

tailscale up --login-server https://headscale.example.com --authkey <the-key>

The --login-server flag is the whole trick — it is the same binary everyone uses, just aimed elsewhere. Confirm the node landed:

docker compose exec headscale headscale nodes list

Repeat tailscale up on every other machine with a fresh key (or reuse one made with --reusable), and they can now reach each other by their 100.64.x.x addresses or their MagicDNS names. No port forwarding, no manual peer config.

Tip — Generate a separate pre-auth key per machine and set a short expiry. A reusable, non-expiring key is convenient and is exactly the kind of thing that ends up pasted into a repo and used to join a stranger's laptop to your network.

Exit nodes and subnet routes

The two features people came for. To let one node route traffic for a whole subnet (say it sits in front of your home LAN), advertise the route on that node:

tailscale up --login-server https://headscale.example.com --advertise-routes=192.168.1.0/24

Then approve it on the server — Headscale does not auto-enable routes, which is a feature, not a bug:

docker compose exec headscale headscale nodes list
docker compose exec headscale headscale routes enable --route <route-id>

An exit node (route all internet traffic through it) is the same dance with --advertise-exit-node, approved the same way.

When plain WireGuard is the right answer instead

Headscale earns its keep when you have many machines, they move between networks, and you would otherwise be hand-editing peer configs constantly. The coordination server exists precisely to kill that toil.

If your setup is a handful of static servers — a VPS, a home box, a backup target — that rarely change and all have reachable addresses, a hand-rolled WireGuard mesh is simpler, has fewer moving parts, and nothing to keep patched beyond the kernel. There is no control plane to go down because there is no control plane. Our WireGuard guide walks through that setup, and for three peers it is genuinely less work than everything above.

The dividing line is churn and NAT. Lots of roaming devices behind NATs? Headscale. A few fixed nodes with public IPs? Plain WireGuard, and don't overthink it.

For either one you want a server with a clean, static IPv4 that is yours alone, since it becomes the anchor of the whole network. That is the kind of box a plan like NoctHost's Micro tier covers comfortably — coordination and DNS filtering barely register on CPU.

Spin one up in about a minute

Email signup, pay with crypto, hourly billing. Trying a box costs cents — destroy it when you are done.

Deploy a server

Frequently asked

Does my VPN traffic go through the Headscale server?
No. Headscale only coordinates — it authenticates devices and tells them how to find each other. The actual encrypted traffic flows directly between peers over WireGuard, or through a DERP relay when a direct path is impossible. Your Headscale box can be tiny because it never carries the payload.
Can I use the official Tailscale apps with Headscale?
Yes, on Linux, macOS, Windows, and Android via the --login-server flag or the equivalent setting. iOS is the awkward exception: the App Store client makes pointing at a custom server harder, and support has varied by version. Check the current Headscale docs before betting an iPhone on it.
What happens if the Headscale server goes offline?
Existing tunnels keep working, because they are peer-to-peer and already established. What you lose while it is down: adding new nodes, re-issuing keys, and refreshing nodes whose keys expire. Treat it as a service that should stay up, but not one whose brief outage cuts your live connections.
Is Headscale as secure as Tailscale?
The WireGuard data plane is identical, so the cryptography protecting your traffic is the same. The difference is that you now own the control plane's security: keeping the server patched, protecting the pre-auth keys, and configuring ACLs correctly. You trade "trust Tailscale's operations team" for "trust your own." That is the entire point, and also the responsibility.

Keep reading