What Headscale Does That Tailscale Doesn’t
Tailscale is one of the cleanest VPN solutions available right now – it handles NAT traversal automatically, uses WireGuard under the hood, and gets devices talking to each other with almost no configuration. The catch is that the coordination server, the piece that authenticates devices and distributes keys, lives entirely on Tailscale’s infrastructure. For personal projects or small teams that’s fine. For anyone running sensitive workloads, operating under strict data-residency requirements, or simply unwilling to depend on a third-party service for network access, that arrangement is a problem. Headscale solves it by reimplementing the Tailscale control server as open-source software you run yourself.
The architecture stays identical to a normal Tailscale deployment. Clients still run the standard Tailscale application – Android, iOS, macOS, Linux, Windows – and still use WireGuard for the actual traffic. Headscale only replaces the coordination layer, meaning your devices authenticate against your server instead of Tailscale’s. You keep full control over which machines join the network, which users exist, and all logs stay on hardware you own. This guide walks through installing Headscale on a Linux VPS, generating a pre-auth key, and connecting your first client.

Server Prerequisites and Installation
You need a publicly reachable Linux server – Ubuntu 22.04 or Debian 12 both work well. The server needs an open TCP port (the default is 8080, though 443 is more practical for production use) and a domain name pointing to its IP address. Headscale itself is distributed as a single binary with no external runtime dependencies, which keeps the setup straightforward. The project publishes official releases on GitHub under the juanfont/headscale repository. Grab the latest release URL, then download and install it directly.
wget https://github.com/juanfont/headscale/releases/download/v0.23.0/headscale_linux_amd64
sudo install --owner root --group root --mode 0755 headscale_linux_amd64 /usr/local/bin/headscale
Once the binary is in place, create the configuration directory and download the example config file from the repository. The config file is a YAML document and the important fields are server_url (the public HTTPS address your clients will reach), listen_addr (the local address and port Headscale binds to), and the db_path for the SQLite database. Set server_url to your domain, for example https://headscale.yourdomain.com. Create the data directory at /var/lib/headscale and ensure a dedicated headscale system user owns it. Running as root is unnecessary and inadvisable.
Configuring TLS and the systemd Service
Headscale can manage its own TLS certificate via Let’s Encrypt by setting tls_letsencrypt_hostname in the config, or you can front it with nginx or Caddy and handle TLS there. The Let’s Encrypt integration is the simpler path for a standalone setup – set the hostname field, point tls_letsencrypt_cache_dir at a writable directory, and Headscale will handle certificate issuance and renewal automatically. If you go the reverse proxy route, set tls_cert_path and tls_key_path to empty strings and configure your proxy to forward traffic on port 443 to Headscale on 8080.
The DERP relay configuration is worth checking before you move on. DERP servers are the relay nodes Tailscale uses when a direct WireGuard connection between two peers cannot be established. By default Headscale uses Tailscale’s public DERP infrastructure, which is fine for most self-hosted deployments. If you want complete independence you can run your own DERP server and point Headscale at a custom DERP map, but that is a separate project and outside the scope of this guide.
Create a systemd unit file at /etc/systemd/system/headscale.service with the following content:
- ExecStart: /usr/local/bin/headscale serve
- User: headscale
- Restart: always
- RestartSec: 5
- AmbientCapabilities: CAP_NET_BIND_SERVICE (only needed if binding below port 1024)
After saving the unit file, run sudo systemctl daemon-reload, then sudo systemctl enable –now headscale. Check the service status with sudo systemctl status headscale and look for any errors in the journal with journalctl -u headscale -f. A successful start will show Headscale listening on the configured address and, if Let’s Encrypt is enabled, it will begin the certificate challenge immediately. Give it thirty seconds and hit your server URL in a browser – you should see a plain JSON response confirming the API is reachable.

Creating Users and Pre-Auth Keys
Headscale organizes devices into namespaces called users. Before any client can register, you need at least one user. The CLI handles this directly on the server:
- Create a user: headscale users create mynetwork
- List existing users: headscale users list
- Generate a pre-auth key: headscale preauthkeys create –user mynetwork –reusable –expiration 24h
The pre-auth key is a string your clients use to authenticate without going through an interactive login flow. The –reusable flag lets multiple devices use the same key, which is useful during initial setup. For production you may prefer single-use keys or ephemeral keys that expire after the device registers. Copy the generated key – you will need it on each client machine.
Connecting Tailscale Clients to Headscale
On any machine that already has the Tailscale client installed, registration against Headscale is a single command. The key is the –login-server flag that points the client at your server instead of Tailscale’s control plane. On Linux run:
sudo tailscale up –login-server https://headscale.yourdomain.com –authkey YOUR_PREAUTHKEY
On macOS and Windows the process has an extra step because the official GUI clients default to Tailscale’s login server and do not expose a flag in the UI. You need to log out the existing client first, then use the CLI equivalent. On macOS that means opening Terminal and running the same tailscale up command with the –login-server flag. On Windows, open an Administrator command prompt and run the same from C:\Program Files\Tailscale\tailscale.exe. iOS and Android require the Tailscale app and do not yet support custom login servers directly – a community-built app called Tailscale (with custom control URL) exists for those platforms, or you can look at the tsapp approach documented in the Headscale wiki.
Once a device connects, verify it appears on the server side with headscale nodes list. You will see the device name, its assigned IP in the 100.x.x.x range, the user it belongs to, and its last-seen timestamp. Devices on the same Headscale instance can communicate directly using those IPs exactly as they would on a normal Tailscale network. If you are also running Uptime Kuma or other self-hosted monitoring tools, putting them behind Headscale means you expose nothing publicly while still reaching them from any registered device.

Maintenance, Routes, and Access Control
Headscale supports subnet routing, which lets a single node advertise a local network range to the rest of the mesh. On the advertising node run sudo tailscale up –login-server https://headscale.yourdomain.com –advertise-routes=192.168.1.0/24, then approve the route on the server with headscale routes enable -r ROUTE_ID. This is the same workflow as Tailscale’s subnet router feature and is useful for reaching devices on a home or office LAN without installing the client on each one.
Access control in Headscale is handled through an ACL file in HuJSON format, the same format Tailscale uses. Define groups, tag owners, and allow or deny traffic between them. Headscale applies the policy at the control layer, so clients receive only the routes and peer information they are allowed to see. For small setups the default policy allows all traffic between users in the same namespace, which is a reasonable starting point. More complex topologies – separate groups for servers and workstations, or contractors with restricted access – are possible through the same ACL file without any additional tooling.
One operational detail that catches people off guard: Headscale does not currently support MagicDNS the way Tailscale’s hosted product does. DNS-based service discovery between nodes is possible, but you need to configure it yourself using a DNS server inside your network or by setting a custom DNS suffix through the Headscale config. The gap is documented in the project’s open issues and has been an active area of development, so the situation may improve in upcoming releases – but going in with accurate expectations avoids confusion when hostnames do not resolve automatically the way they would on Tailscale’s own service.





