Self-Host the Tailscale Control Plane with Headscale
Run your own Tailscale control plane with Headscale: install, configure TLS, register nodes with pre-auth keys, and enforce ACL policies on your infrastructure.
Before you start
- ▸A VPS or server with a public static IP and a DNS A record pointing to it
- ▸A registered domain name for TLS certificate issuance
- ▸Tailscale client installed on every node you plan to register
- ▸Root or sudo access on both the Headscale server and each node
Tailscale's client software is open source, but the coordination server—the piece that distributes WireGuard keys, manages peer discovery, and enforces ACLs—is proprietary SaaS. Headscale is a self-hosted, open-source reimplementation of that control plane. You run it on your own server, your nodes authenticate to it instead of login.tailscale.com, and you control everything: key expiry, ACLs, user namespaces, and data residency. The trade-off is real: no Tailscale GUI, no MagicDNS via the SaaS dashboard, and you are on the hook for availability and upgrades.
How Headscale Differs from Tailscale SaaS
Both use the Tailscale client (tailscale / tailscaled) on every node—nothing changes there. The difference is where the control plane lives. Tailscale SaaS handles DERP relay servers globally; Headscale lets you point at Tailscale's public DERP map (default) or run your own DERP servers. Headscale does not yet support Tailscale's newer features the moment they ship—expect a lag of weeks to months. Check the client compatibility matrix before choosing a client version.
Prerequisites and Architecture Decisions
- A publicly reachable server (VPS or bare metal) with a static IP and a DNS A record pointing to it. Headscale must be reachable by all nodes on TCP 443 (or 8080 if you front it with a reverse proxy).
- A domain name—nodes authenticate via HTTPS, so a valid TLS certificate is required. This guide uses nginx + Certbot.
- Root or sudo access on the Headscale server.
- The Tailscale client pre-installed on every node you want to register.
Step 1: Install Headscale
Headscale ships as a single static binary. The project publishes .deb, .rpm, and raw binaries on GitHub Releases. Always use the latest stable release—avoid building from main in production.
Debian / Ubuntu
HEADSCALE_VERSION="0.23.0"
curl -fsSLo /tmp/headscale.deb \
"https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_amd64.deb"
sudo dpkg -i /tmp/headscale.deb
Fedora / RHEL / Rocky
HEADSCALE_VERSION="0.23.0"
curl -fsSLo /tmp/headscale.rpm \
"https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_amd64.rpm"
sudo rpm -i /tmp/headscale.rpm
Arch Linux
yay -S headscale-bin
The package creates a headscale system user, drops the binary at /usr/bin/headscale, places a default config at /etc/headscale/config.yaml, and installs a systemd unit.
Step 2: Configure Headscale
Edit /etc/headscale/config.yaml. The minimum fields you must change are shown below; leave everything else at its default for a first deployment.
sudo nano /etc/headscale/config.yaml
# Hostname nodes will reach the control plane at
server_url: https://hs.example.com
# Address headscale binds to locally (nginx proxies to this)
listen_addr: 127.0.0.1:8080
metrics_listen_addr: 127.0.0.1:9090
# Directory that must exist and be writable by the headscale user
db_path: /var/lib/headscale/db.sqlite
# Issued to nodes; pick a range that does not clash with your LAN
ip_prefixes:
- 100.64.0.0/10
# Use Tailscale's hosted DERP map (simplest option)
derp:
urls:
- https://controlplane.tailscale.com/derpmap/default
Step 3: TLS via nginx Reverse Proxy
Headscale can terminate TLS itself, but fronting it with nginx gives you easier certificate management and lets you host other services on the same machine.
sudo apt install nginx certbot python3-certbot-nginx # Debian/Ubuntu
sudo dnf install nginx certbot python3-certbot-nginx # Fedora/RHEL
sudo certbot --nginx -d hs.example.com
Add a dedicated server block for Headscale. The gRPC-web and long-poll paths need specific proxy settings:
cat /etc/nginx/sites-available/headscale
# server block contents below — paste into that file
server {
listen 443 ssl http2;
server_name hs.example.com;
ssl_certificate /etc/letsencrypt/live/hs.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/hs.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400s;
}
}
sudo ln -s /etc/nginx/sites-available/headscale /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Step 4: Start and Enable Headscale
sudo systemctl enable --now headscale
sudo systemctl status headscale
Check the logs if it fails to start:
sudo journalctl -u headscale -n 50 --no-pager
Step 5: Create a User and Register Nodes
Headscale uses "users" (previously called namespaces) to group nodes. Create one per logical group or per human user.
sudo headscale users create alice
Generate a Pre-Auth Key
Pre-auth keys let you register nodes non-interactively—essential for automation. The --reusable flag allows multiple nodes to use the same key; omit it for single-use keys.
sudo headscale preauthkeys create --user alice --reusable --expiration 24h
Copy the key that is printed. It will not be shown again.
Register a Node
On each node (not the Headscale server itself, unless you want that too), run:
sudo tailscale up \
--login-server https://hs.example.com \
--authkey <PASTE_KEY_HERE>
The node appears in your Headscale database within seconds. Confirm:
sudo headscale nodes list
Output will look similar to:
ID | Hostname | Name | User | IP addresses | Ephemeral | Last seen
1 | web01 | web01 | alice | 100.64.0.1 | false | 2024-11-01 09:14:22
Step 6: Define ACLs
Headscale uses the same HuJSON ACL format as Tailscale. The policy file lives at the path set by acl_policy_path in your config (default: /etc/headscale/acls.hujson). Without an explicit policy, nodes in the same user can reach each other; nodes across users cannot by default.
cat /etc/headscale/acls.hujson
{
"groups": {
"group:admin": ["alice"],
"group:servers": ["bob"]
},
"acls": [
// Admins can reach everything
{ "action": "accept", "src": ["group:admin"], "dst": ["*:*"] },
// Servers can only talk to each other on SSH
{ "action": "accept", "src": ["group:servers"], "dst": ["group:servers:22"] }
]
}
Apply the policy without restarting the daemon:
sudo headscale policy set --path /etc/headscale/acls.hujson
Step 7: Verify Connectivity
From a registered node, confirm the Tailscale interface is up and peers are visible:
tailscale status
tailscale ping 100.64.0.2
A successful response shows round-trip latency and confirms WireGuard tunnels are forming correctly. If the ping shows "via DERP" instead of a direct path, check that UDP is not blocked on both nodes' firewalls.
Firewall Notes
On the Headscale server, open TCP 443 (or 80 for the ACME challenge). Nodes communicate peer-to-peer over WireGuard (UDP, typically port 41641). If you use firewalld:
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --add-port=41641/udp
sudo firewall-cmd --reload
With ufw (Debian/Ubuntu):
sudo ufw allow 443/tcp
sudo ufw allow 41641/udp
Troubleshooting
- Node shows as disconnected immediately: Verify
server_urlinconfig.yamlexactly matches the URL passed totailscale up --login-server, including the scheme. - TLS handshake errors on nodes: Your certificate must be from a trusted CA. Self-signed certs require setting
SSLKEYLOGFILEworkarounds or adding the CA to each node's trust store—use Let's Encrypt instead. - Nodes cannot reach each other despite being registered: Check your ACL policy. A missing or over-restrictive policy silently drops traffic. Run
sudo headscale policy getto confirm what is loaded. - Pre-auth key rejected: Keys are time-limited and case-sensitive. Regenerate with a longer
--expirationif automating. - High DERP latency: Tailscale's public DERP servers are geographically distributed but you have no control over them. For latency-sensitive deployments, run a DERP server in your own region and add it to
derp.serverin your config.
Frequently asked questions
- Can I use the official Tailscale mobile or desktop clients with Headscale?
- Yes, but with caveats. iOS and macOS clients require using the 'custom control server' option available in newer app versions. Android users can use the official client or the open-source Tailscale app with a custom server URL. Always check Headscale's client compatibility matrix for the version of the client you intend to use.
- Does Headscale support MagicDNS?
- Headscale has basic DNS integration via the dns config block in config.yaml, allowing you to set a magic_dns base domain and override DNS nameservers pushed to nodes. It is not as fully featured as Tailscale's SaaS MagicDNS, but covers most use cases.
- What happens to my nodes if the Headscale server goes down?
- Existing WireGuard tunnels between nodes stay up because WireGuard is peer-to-peer and requires no central broker for established sessions. New nodes cannot register, and key rotations will fail until the server is restored.
- How do I migrate from Tailscale SaaS to Headscale without re-keying every node?
- There is no import path for existing Tailscale SaaS keys—WireGuard keypairs are generated per control plane. You must run tailscale up --login-server on each node, which generates a new WireGuard identity and a new Tailscale IP. Plan for an IP change on all nodes.
- Can Headscale and Tailscale SaaS nodes communicate?
- No. Nodes registered to Headscale and nodes registered to Tailscale SaaS are on entirely separate control planes and cannot exchange WireGuard keys with each other. You would need to use a subnet router or traditional VPN to bridge the two networks.
Related guides
Build a Mesh VPN with Nebula
Build a fully self-hosted mesh VPN with Nebula: create a CA, sign node certs, configure lighthouses, enforce group-based firewall rules, and run as a systemd service.
Common Linux Network Ports Reference
Learn Linux port ranges, read /etc/services, find what's listening with ss and nmap, and apply solid firewall rules to expose or block the right ports.
How to Configure a Static IP on Linux
Configure a static IP on Linux using Netplan, NetworkManager (nmcli), or systemd-networkd across Ubuntu, Fedora, Debian, and Arch with verified steps.
Expose a Service with Cloudflare Tunnel
Expose local services to the internet without port-forwarding using Cloudflare Tunnel. Install cloudflared, create a named tunnel, configure ingress rules, and run as a systemd service.