$linuxjunkies
>

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.

AdvancedUbuntuDebianFedoraArch12 min readUpdated June 7, 2026

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_url in config.yaml exactly matches the URL passed to tailscale up --login-server, including the scheme.
  • TLS handshake errors on nodes: Your certificate must be from a trusted CA. Self-signed certs require setting SSLKEYLOGFILE workarounds 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 get to confirm what is loaded.
  • Pre-auth key rejected: Keys are time-limited and case-sensitive. Regenerate with a longer --expiration if 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.server in your config.
tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

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