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.comthat 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.1Two 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 -dRegistering 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 1hThat 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 listRepeat 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.
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/24Then 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.