Headscale
Self-hosted reimplementation of the Tailscale control server for official Tailscale clients
Headscale is an independent implementation of Tailscale's coordination server, the piece Tailscale Inc does not let you run yourself. You keep using the official Tailscale clients and point them at your own server, so key exchange and device metadata stay on your hardware.
Our verdict on Headscale#
Headscale is a clean-room reimplementation of Tailscale's coordination server, not a fork of the client. Every device keeps running the official Tailscale client and joins with tailscale up --login-server=https://hs.example.com. That is the whole proposition: MagicDNS, ACLs and NAT traversal without your device inventory living at Tailscale Inc. The README states the project is not associated with Tailscale Inc, though one maintainer is employed there and contributes during work hours. Take the version coupling seriously. v0.29.3 (July 2026) documents a minimum Tailscale client of v1.80.0, and the project is still pre-1.0 with real breaking changes between minors: you must upgrade one minor at a time (0.26 to 0.27.1 to 0.28.0 to 0.29) because migrations for databases older than 0.25.0 were removed in 0.28. The FAQ is refreshingly blunt: it targets self-hosters, expects hundreds of nodes, and tells you to use SQLite because PostgreSQL is in maintenance mode.
Who Headscale is for#
Choose it if
- Someone already happy with Tailscale who wants the coordination server on their own box without changing anything on the clients
- A homelab needing remote access to a handful of machines behind CGNAT, where WireGuard port forwarding is not an option
- Anyone who wants OIDC-gated device enrollment tied to their existing Authentik or Keycloak
- Small teams (tens of devices) comfortable driving user and node management from a CLI
Look elsewhere if
- You want a web admin UI; there is no official one, only the CLI and a gRPC API, with third-party UIs of varying quality
- You need the commercial feature set (Tailscale SSH audit, Funnel, app connectors) or a support contract
- You are running hundreds of chatty nodes, since the maintainers explicitly deprioritize performance in favour of maintainability
What running it actually looks like#
One container or one binary, plus a reverse proxy. Headscale listens on 127.0.0.1:8080 by default and expects TLS terminated in front of it on 443, because clients require HTTPS and server_url must exactly match the name they dial. State lives in /var/lib/headscale: db.sqlite, noise_private.key and the DERP key. Configuration is one YAML file at /etc/headscale/config.yaml, and you should take the example from the git tag matching the version you run, not from main. base_domain for MagicDNS must be a different domain from the one in server_url or startup fails. There is no web UI: users, pre-auth keys, node approval and route enabling are all headscale subcommands, or gRPC on 50443 if you enable it. Metrics sit on 9090. Turning on the embedded DERP relay also means opening udp/3478 for STUN.
Resource profile#
- Memory
- 40 MB idle, around 120 MB in ordinary use. Memory tracks node count and how often the network map changes; a tailnet of 20 mostly-idle devices sits near the low figure.
- CPU and acceleration
- Quiet until nodes churn. Every join, leave or route change recomputes and pushes a network map to every peer, which is why the maintainers cap expectations at hundreds of nodes rather than thousands.
- Storage growth
- A few megabytes. db.sqlite grows with nodes and routes, not with traffic, since data plane traffic never touches the server unless you run the embedded DERP relay.
- Operational load
- 3 of 5, Moderate. Small and quiet in steady state, but pre-1.0 with breaking changes between minors, a documented no-skipping upgrade path, and a moving minimum client version. Upgrades are read-the-notes events, not tag bumps.
Figures describe a small single-household install and are the working assumptions behind our stack planner. Your numbers will differ with library size, user count and hardware. See methodology.
Data and backup surface#
Everything worth keeping is in /var/lib/headscale plus /etc/headscale/config.yaml. db.sqlite holds users, nodes, subnet routes, pre-auth keys and (if you store it there) the ACL policy; noise_private.key is the server's identity, and losing it forces every node to re-register from scratch. If you use a file-based ACL policy, back that file up too. Stop headscale or use sqlite3 db.sqlite ".backup out.db" before copying, since a live cp of an open WAL database can restore inconsistent. The whole set is a few megabytes for a home tailnet. None of it is regenerable: node keys live on the clients, but the mapping of node to user, tag and 100.64.x.x address exists only here.
Derived backup shape
SQLite backup. sqlite3 <db> ".backup out.db" while running, or stop the container and copy. A live cp of a WAL-mode SQLite file can restore corrupt.
Traps and surprises#
You cannot skip minor versions on upgrade
Headscale requires stepping through each stable minor in order, for example 0.26.0 to 0.27.1 to 0.28.0 to 0.29.x. 0.28 removed database migrations for anything older than 0.25.0, so a machine that has been sitting on an old release cannot jump straight to current: it starts, fails the migration and leaves you restoring the backup. Pull each intermediate image, start it once, confirm it comes up, then move on.
The minimum Tailscale client version keeps moving
v0.29.3 documents a floor of Tailscale client v1.80.0. Distro-packaged tailscaled on an older stable release is frequently below that, and the failure mode is a registration that hangs or a node that connects and then never gets a working network map. Check client versions across your fleet before upgrading the server, and prefer Tailscale's own repositories over the distro package on long-lived machines.
base_domain must differ from the server_url domain
MagicDNS names are issued under base_domain. If you set server_url: https://vpn.example.com and base_domain: example.com, headscale refuses to start rather than create a resolution loop where clients cannot reach the control server they are configured against. Use something like server_url: https://headscale.example.com with base_domain: ts.example.net, or a dedicated internal suffix.
0.29 redefined the ACL wildcard
In 0.29 a bare * in a policy resolves to the Tailscale CGNAT range 100.64.0.0/10 plus the IPv6 ULA range, not every address. Rules that previously granted a group access to LAN subnets or internet destinations through an exit node stop working after the upgrade, and the symptom is a healthy tailnet with newly broken subnet routing. Rewrite those rules with explicit CIDRs first. The same release changed hostname collision suffixes from random strings to numeric ones (laptop-1).
No support relationship with Tailscale Inc
The README says the project is not associated with Tailscale Inc. The control protocol is Tailscale's and it moves; headscale follows. In practice that means a client auto-update can land a version headscale has not caught up with, so pin or delay client updates on machines you cannot physically reach, and read headscale release notes before letting your fleet upgrade itself.
Specifications#
- Category
- Networking, DNS and remote access
- Licence
- BSD-3-Clause (Permissive)
- Written in
- Go
- First release
- 2021
- Maturity
- Young
- Datastore
- SQLite (recommended); PostgreSQL supported but in maintenance mode
- Default ports
8080,9090,3478- Ships as
- docker, deb package, native binary
- arm64 builds
- Yes
- Identity
- Native OIDC. Built-in OIDC: point it at Authentik, Keycloak or Google and node registration goes through your IdP instead of pre-auth keys.
- Replaces
- Tailscale, ZeroTier Central, Twingate, Cloudflare WARP
- Project site
- headscale.net
- Source
- github.com/juanfont/headscale
- Documentation
- headscale.net/stable
Alternatives to Headscale#
Everything else in networking, dns and remote access, closest in operational weight first.
| Tool | Ops load | Idle RAM | Licence |
|---|---|---|---|
| Technitium DNS Server | 3, Moderate | 130 MB | GPL-3.0 |
| Traefik | 3, Moderate | 60 MB | MIT |
| AdGuard Home | 2, Light | 60 MB | GPL-3.0 |
| Nginx Proxy Manager | 2, Light | 100 MB | MIT |
| Pi-hole | 2, Light | 100 MB | EUPL-1.2 |
| wg-easy | 2, Light | 90 MB | AGPL-3.0-only |
Where Headscale comes up elsewhere#
Head to head
Guides
Reference
Sources#
Facts on this page were checked against the project's own material. Where a number is our own estimate rather than a published figure, the text says so.
Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.