Headscale: Self-Hosting the Tailscale Control Plane
Our WireGuard guide covers building a tunnel by hand, and our VPN comparison covers where Tailscale fits. This covers removing the one remaining third party.
What the coordination server does
Worth being precise, because people assume more than is true.
Tailscale is WireGuard underneath. The data path is direct, node to node, encrypted with WireGuard. Tailscale’s servers do not see your traffic.
What the coordination server does is solve the problem that makes plain WireGuard painful at scale:
Key distribution. Every node needs every other node’s public key. With ten nodes that is ninety entries maintained by hand.
Endpoint discovery. Nodes move between networks and change addresses. Something must tell the others where a node is now.
NAT traversal coordination. Two nodes behind NAT need a third party to help them find each other.
Access policy. Which nodes may reach which.
That is real work, and it is why Tailscale is pleasant where hand-rolled WireGuard is not.
The tradeoff is that a company knows your device inventory: which machines exist, their names, who owns them, when they connect. Not the traffic, and not nothing either.
Headscale reimplements that coordination role so it runs on your server.
Running it
services:
headscale:
image: headscale/headscale:0.26
command: serve
volumes:
- ./config:/etc/headscale
- ./data:/var/lib/headscale
ports:
- "127.0.0.1:8080:8080"
- "3478:3478/udp"
restart: unless-stopped
# config/config.yaml
server_url: https://vpn.example.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
dns:
magic_dns: true
base_domain: tailnet.example.com
nameservers:
global:
- 1.1.1.1
derp:
urls:
- https://controlplane.tailscale.com/derpmap/default
server:
enabled: false
Port 3478/UDP is STUN, used for NAT traversal. Publish it or direct connections fail more often and everything falls back to relaying.
server_url must be HTTPS and reachable by clients. Put it behind a reverse proxy with a real certificate.
MagicDNS is what gives you ssh web01 instead of remembering addresses, and it is one of the nicest things about the whole system.
Connecting clients
The official clients work unchanged, which is what makes this practical.
# on the server
docker compose exec headscale headscale users create colton
docker compose exec headscale headscale preauthkeys create --user colton --expiration 1h --reusable
# on the client
sudo tailscale up --login-server https://vpn.example.com --authkey <key>
Or interactively:
sudo tailscale up --login-server https://vpn.example.com
# prints a URL; register it on the server:
docker compose exec headscale headscale nodes register --user colton --key <nodekey>
docker compose exec headscale headscale nodes list
tailscale status
tailscale ping web01
tailscale ping reports whether the connection is direct or via DERP. Direct is what you want; persistent relaying means NAT traversal is failing and is worth investigating.
Access control
{
"acls": [
{
"action": "accept",
"src": ["group:admins"],
"dst": ["*:*"]
},
{
"action": "accept",
"src": ["group:devs"],
"dst": ["tag:dev-server:22,80,443"]
},
{
"action": "accept",
"src": ["tag:monitoring"],
"dst": ["*:9100,9090"]
}
],
"groups": {
"group:admins": ["colton"],
"group:devs": ["alice", "bob"]
},
"tagOwners": {
"tag:dev-server": ["group:admins"],
"tag:monitoring": ["group:admins"]
}
}
This is the feature that makes a mesh VPN better than a flat one. Everyone on a traditional VPN can reach everything on it; here, developers reach dev servers on three ports and nothing else.
The rules are enforced by the nodes themselves, distributed by the coordination server, so there is no chokepoint to route through.
Subnet routers and exit nodes
# advertise a LAN behind one node
sudo tailscale up --login-server https://vpn.example.com \
--advertise-routes=192.168.1.0/24
# approve it
docker compose exec headscale headscale routes enable --identifier 3 --route 192.168.1.0/24
One node bridges a whole network, so a printer or a NAS that cannot run a VPN client becomes reachable.
sudo tailscale up --advertise-exit-node
sudo tailscale up --exit-node=home-router
Exit nodes route all traffic, which is the use case people usually mean by VPN. Note the --advertise-exit-node side needs IP forwarding enabled, which our networking basics guide covers.
DERP
When two nodes cannot connect directly, traffic relays through DERP. It is still end-to-end encrypted; the relay carries ciphertext.
By default Headscale uses Tailscale’s public relays, which means a third party carries your bytes in the fallback case. Running your own closes that:
derp:
server:
enabled: true
region_id: 999
region_code: "self"
stun_listen_addr: "0.0.0.0:3478"
Whether it is worth it depends on how often you actually fall back to relaying, which tailscale status will tell you. For most setups with reasonable NAT, rarely.
What you give up
Being honest about it.
No web interface. Everything is headscale on the command line. Third-party UIs exist and are not official.
No support. It is volunteers.
Feature lag. Headscale tracks the protocol and trails the commercial product, particularly on newer features.
The project says so itself. Headscale is explicitly not positioned as a drop-in commercial replacement at scale.
For a homelab or a small team it works well. For an organisation that needs an admin interface and someone to call, the commercial product is the honest answer, and its free tier is generous.
Where it fits
The combination worth noting: Headscale plus Authelia covers both access models. The VPN handles machines you control, and SSO handles people you want to share a service with who will not install a client.
Our self-hosting introduction argues that reaching services over a VPN beats exposing them, and a mesh VPN is what makes that convenient enough to actually do. The reason people expose services is that VPNs were annoying; this removes the excuse.
Frequently Asked Questions
What does the Tailscale coordination server actually do?
It distributes public keys and endpoint addresses so nodes can find each other, and it enforces access policy. It never sees your traffic, because the data path is direct WireGuard between nodes. Headscale reimplements that coordination role so you can run it yourself.
Does self-hosting the control plane mean my traffic stops going through Tailscale?
Your traffic never went through Tailscale in the first place, except when a direct connection cannot be established and a DERP relay is used. What changes is that the coordination metadata, meaning which devices exist and who owns them, stays on your own server.
What is a DERP relay and do I need my own?
DERP relays traffic when two nodes cannot establish a direct connection, typically behind strict NAT. Headscale can use Tailscale’s public relays by default, and running your own keeps even that fallback path on your infrastructure at the cost of hosting it.
What do I lose by using Headscale instead of Tailscale?
The admin web interface, official support, and some features that arrive in the commercial product first, including parts of the tailnet lock and app connector functionality. Headscale tracks the open protocol and lags on newer features.
Can I use the official Tailscale clients with Headscale?
Yes, that is the design. The clients are open source and you point them at your Headscale URL with the login-server flag. No modified client is needed, which is what makes Headscale practical rather than a full reimplementation.
Is Headscale suitable for a company?
It is a community project maintained by volunteers and is explicitly not intended as a drop-in commercial replacement at scale. For a homelab or a small team it works well, and for an organisation needing support and an admin interface the commercial product is the honest answer.