What Headscale does for your devices
Every Tailscale network has a coordinator: the server that knows which devices belong, which addresses they get and which routes they offer. Headscale is an open-source version of that coordinator that you run yourself. Your devices keep using the Tailscale client they already know; you just tell it to log in to your server instead.
It is aimed at self-hosters, home labs and small open-source groups, and it runs one tailnet per server. The licence is BSD 3-Clause, so there is nothing to buy, and Kristoffer Dalby and Juan Font maintain it.
Headscale is an independent project. Its README carries a disclaimer, "not associated with Tailscale Inc.", and is open about one detail: an active maintainer works for Tailscale and is allowed to contribute during work hours.
How big a server does it need?
A small one, for most people. The project gives no hardware minimum. What costs effort is change: each time a device joins, leaves or moves networks, Headscale rebuilds the network map for every device. The FAQ puts it plainly that hundreds of quiet devices are usually fine, while lots of devices that keep moving, like a team of laptops, keep it working constantly.
One small machine that does only this job is the natural fit. On our dedicated servers you get the whole machine with root access, an unmetered port, and IPv6 at no charge, which matters here because the project recommends a public IPv4 and IPv6 address. It is month to month, so you can start small and move up later.
Keep the Headscale server out of its own tailnet. The FAQ warns that installing the Tailscale client on the same machine can upset subnet routers, relays and MagicDNS, and the setup is unsupported.
Which ports to open
- 443/tcp, public. Your devices talk to Headscale here over HTTPS. Stick to 443: the Tailscale app assumes it in some situations.
- 80/tcp, public. Let's Encrypt checks your domain here before issuing Headscale a certificate.
- 3478/udp, public, only with the built-in relay. STUN, which helps devices find their public address and connect directly.
- 9090/tcp, never public. Metrics and a debug page that lists your connected devices and your access policy.
- 50443/tcp, normally closed. Remote control over gRPC, needed only if you manage Headscale from another machine.
Up and running in an evening
This route has Headscale request and renew a Let's Encrypt certificate by itself, so there is no web server to configure. First create a DNS record, such as vpn.example.org, that resolves to the server (plus an AAAA record when the server has IPv6).
- Turn the firewall on with only SSH and the Headscale ports allowed, setting IPv6 support first so each rule covers both address families.
- Download the package for your version and architecture from the releases page and install it. It creates a
headscaleuser and a systemd service. - Open the configuration file and set the values shown in the second block.
sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw allow OpenSSH
sudo ufw allow 80,443/tcp
sudo ufw allow 3478/udp
sudo ufw --force enable
HEADSCALE_VERSION="0.29.5" # newest on the releases page, no "v"
HEADSCALE_ARCH="amd64"
wget --output-document=headscale.deb \
"https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb"
sudo apt install ./headscale.deb
sudo nano /etc/headscale/config.yaml
Change these keys in the existing file rather than adding copies of them:
server_url: https://vpn.example.org
listen_addr: ":443"
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 127.0.0.1:50443
tls_letsencrypt_hostname: vpn.example.org
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ":http"
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
noise:
private_key_path: /var/lib/headscale/noise_private.key
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
derp:
server:
enabled: true
stun_listen_addr: "0.0.0.0:3478"
ipv4: <YOUR_PUBLIC_IPV4>
ipv6: <YOUR_PUBLIC_IPV6>
dns:
base_domain: tailnet.example.net
server_urlis the address your devices use. Your MagicDNS name,dns.base_domain, has to be a different domain.- Writing
:443with no address makes Headscale listen on every address the server has, IPv4 and IPv6 (that is how Go, the language Headscale is written in, treats an empty host; our note, not the docs'). The example file's own value, 127.0.0.1:8080, only answers locally, so it has to change. - The noise key secures the connection between Headscale and each device. If the file is missing, Headscale creates it.
- Your devices take addresses from
prefixes. Only ranges inside the two defaults shown are supported. - The relay block switches on Headscale's own DERP server, which carries traffic when two devices cannot reach each other directly. Leave it off and your devices use Tailscale's public relays, which stay in the list either way.
Check the file with sudo headscale configtest, restart the service (sudo systemctl restart headscale) and open https://vpn.example.org/health in a browser. The service runs as the headscale user, and its packaged systemd service file carries UMask=0077, which is why the key files it writes in /var/lib/headscale should show -rw-------; if one does not, sudo chmod 600 it.
Bringing people and devices in
Devices belong to Headscale users. Create one with sudo headscale users create maria and look up its ID with sudo headscale users list. Then choose how each device joins:
- With approval. On the device, run
tailscale up --login-server https://vpn.example.org. The page that opens shows an auth ID; approve it on the server withsudo headscale auth register --auth-id AUTH_ID --user maria. - With a pre-auth key.
sudo headscale preauthkeys create --user ID(the numeric ID from the list) gives you a one-use key that expires after an hour unless you change the defaults. On the device, runtailscale up --authkey KEY --login-server https://vpn.example.org.
Handle pre-auth keys like passwords. Anyone holding a live one can add a device to your network, and a key typed on a command line is saved in shell history and visible to other users through ps. Don't paste keys into chat, screenshots or support tickets, and leave the one-use, one-hour default in place unless a script truly needs longer.
If your team already signs in through an identity provider, Headscale can use it over OpenID Connect. Register a confidential client there with https://vpn.example.org/oidc/callback as the redirect, then give Headscale's oidc settings the issuer URL, the client ID and the secret, and switch PKCE on. According to the project's .goreleaser.yml release build file, config.yaml is installed with mode 0644, readable by every user on the box, so put the secret in its own mode-600 file that the headscale user owns, and name that file in client_secret_path. Both authentik and Keycloak are on the project's fully supported list, and allowed_domains, allowed_users or allowed_groups limit who may join.
Until you load a policy file, every device on the tailnet can reach every other one. Add access rules before you invite anyone you would not hand your whole network to.
Already running a website on that server?
Then a reverse proxy owns port 443 and passes Headscale's traffic along. Move Headscale to listen_addr: 127.0.0.1:8080, set tls_letsencrypt_hostname back to an empty string and leave both certificate paths (tls_key_path, tls_cert_path) empty, so the proxy handles TLS and port 80, and list the proxy's address in trusted_proxies so logs show real client IPs.
The proxy must carry WebSockets, with two unusual details: the Tailscale app opens the upgrade with a POST request, and it asks for the tailscale-control-protocol upgrade rather than the usual one. The project's reverse-proxy page has examples, but it is written by the community and not checked by the maintainers, and the README says the project does not support or encourage running Headscale behind a proxy. The FAQ also admits the maintainers have no proxy experience of their own. Caddy carries WebSockets on its own, nginx needs the Upgrade and Connection headers passed through, and Apache needs upgrade=any on its ProxyPass line. Cloudflare's proxy and tunnels cannot carry it at all. STUN on 3478/udp stays outside the proxy, and gRPC cannot go through it.
Backups, keys and upgrades
Everything that makes your network yours lives in two folders: the settings in /etc/headscale, and the SQLite database plus the noise and DERP private keys in /var/lib/headscale. Stop the service, copy both with permissions kept, start it again:
sudo systemctl stop headscale
TIMESTAMP=$(date +%Y%m%d%H%M%S)
sudo cp -aR /etc/headscale /etc/headscale.backup-$TIMESTAMP
sudo cp -aR /var/lib/headscale /var/lib/headscale.backup-$TIMESTAMP
sudo systemctl start headscale
sudo ls -l /var/lib/headscale.backup-$TIMESTAMP
Those copies sit on the same disk, so move them to another machine you control, and keep them as private as the keys they hold. You manage the server; we keep the hardware and the network running.
Upgrades need patience. The project requires stepping through each minor release in turn, always taking its newest patch, because the database changes as you go. For each step: read the release notes, stop Headscale, take the backup above, install the next .deb (its postinst script restarts the service straight away), compare your file against the fresh example the package puts in /usr/share/doc/headscale/examples/, then test and restart. One warning: going by the package's postrm (removal) script, apt purge wipes /var/lib/headscale and removes the headscale account.
Prefer containers?
Images are published on GitHub Container Registry (ghcr.io/juanfont/headscale) and Docker Hub (docker.io/headscale/headscale); even so, the maintainers say they do not officially support Docker installs, and the container guide comes from the community. The README goes further: it neither supports nor encourages containers or proxies for Headscale. If you go that way, publish ports the way that guide does, as 127.0.0.1:8080:8080, with a proxy in front of it. Drop the 127.0.0.1 part and Docker opens that port to the world, on 0.0.0.0 for IPv4 and [::] for IPv6; Docker routes published ports before ufw's rules are applied, so ufw does not protect them. From Docker Engine 28, a 127.0.0.1 mapping really is local-only. For a simple tunnel between two sites, a plain WireGuard VPN server may be all you need.
Questions people ask
- Is Headscale an official Tailscale product?
- No. It is an independent open-source project, and the disclaimer in its README says so directly. You still use Tailscale's own client apps on each device.
- Will phones and Windows PCs connect to it?
- Yes. The project lists Android, iOS, macOS, tvOS and Windows as supported alongside Linux and the BSDs. Windows and Apple devices need a few extra settings, which your own Headscale explains at its
/appleand/windowsaddresses. - Do I have to run my own relay?
- No. Out of the box, devices fall back on the public DERP relays Tailscale Inc. runs. Turning on the built-in relay adds yours to that list; removing theirs leaves you depending on one server.
- Can I skip versions when I upgrade?
- No. Go from one minor release to the next, using the newest patch of each, and back up before every step. The release notes on GitHub list anything that needs changing in your config.
- Can this same server be my exit node?
- Better not. The project does not support running the Tailscale client on the Headscale machine. Make another device the exit node; exit nodes are approved on the server before anyone can use them.