Why Run Your Own Tailscale Control Server
Tailscale makes private networking genuinely easy – you install an agent, log in, and your devices find each other across NAT and firewalls without any manual tunnel configuration. The catch is that every connection is brokered through Tailscale’s coordination servers, which handle key exchange, device registration, and access control. For most people, that trade-off is fine. For anyone running a home lab, a small business, or infrastructure that handles sensitive data, handing that coordination layer to a third-party service is a dependency worth eliminating.
Headscale is an open-source, self-hosted reimplementation of the Tailscale control plane. It speaks the same protocol that official Tailscale clients use, which means you can run the unmodified Tailscale app on any device – Android, iOS, Windows, macOS, Linux – and point it at your own server instead of Tailscale’s. Your traffic still flows directly between devices over WireGuard. Headscale only handles the coordination: key exchange, IP assignment, and policy distribution.
This guide walks through a complete Headscale setup on a Linux server, including client enrollment and a basic ACL policy.

Server Requirements and Installation
Headscale runs as a single binary with no external dependencies beyond a writable directory for its SQLite database and a TLS certificate. A small VPS with 1 vCPU and 512 MB of RAM handles dozens of clients without strain. The server needs a public IP address or a domain with a valid TLS certificate – Headscale clients verify the certificate on connection, so a self-signed cert requires extra steps on every client. Using Let’s Encrypt via a reverse proxy like Caddy or Nginx is the cleanest path.
Download the latest Headscale binary from the project’s GitHub releases page and move it to /usr/local/bin/headscale. Create a dedicated system user with useradd –system –no-create-home headscale, then create the configuration directory at /etc/headscale/ and the data directory at /var/lib/headscale/, both owned by that user. Copy the example configuration file from the repository to /etc/headscale/config.yaml – this file controls the server URL, database path, DNS settings, and DERP relay configuration. The server_url field must match the public URL your clients will reach, including the port if you are not running on 443.
Create a systemd unit file at /etc/systemd/system/headscale.service with the following content, then enable and start the service. Set User=headscale, ExecStart=/usr/local/bin/headscale serve, and Restart=unless-stopped in the service section. After running systemctl enable –now headscale, check the status with systemctl status headscale and confirm the process is listening on the configured port with ss -tlnp. If you are using Caddy as a reverse proxy, a two-line Caddyfile pointing your domain to localhost on the Headscale port handles TLS automatically.
Creating Users and Enrolling Clients
Headscale organizes devices into namespaces called users, which map to the user concept in the official Tailscale service. Before enrolling any device, create at least one user with headscale users create myuser. You can list existing users at any time with headscale users list. Each device you enroll must belong to a user, and ACL policies can reference users to control which devices can communicate with each other.
Enrolling a client requires generating a pre-authentication key from the server and using it during the Tailscale client login. On the server, run headscale –user myuser preauthkeys create –reusable –expiration 24h to generate a key. On the client device, install the standard Tailscale client, then run tailscale up –login-server https://your.headscale.domain –authkey tskey-auth-YOURKEY. The client registers with your server, receives a WireGuard IP in the 100.64.0.0/10 range, and starts routing traffic directly to other enrolled devices. You can confirm successful enrollment on the server with headscale nodes list, which shows the device name, IP address, user, and last-seen timestamp.
Mobile clients on iOS and Android require a slightly different approach. The official Tailscale iOS app does not expose a login server field in the standard UI, but you can trigger a custom server login by opening the Tailscale app, tapping your account name, selecting “Use custom coordination server,” and entering your Headscale URL. The Android app follows the same pattern. Alternatively, generate a pre-auth key and use tailscale up via Termux or a shell app if you prefer CLI enrollment on mobile. Once enrolled, mobile clients behave identically to desktop ones – no special configuration is required after the initial login.

Configuring ACL Policies
By default, Headscale allows all enrolled devices to communicate with each other. That works fine for a personal setup, but any multi-user or multi-team deployment benefits from explicit access control. Headscale uses the same HuJSON policy format as Tailscale, stored in a file you reference from config.yaml under the acl_policy_path key. HuJSON is standard JSON with comments allowed, which makes the policy file easier to annotate and maintain.
A minimal policy file defines groups and access rules. Set a group like “group:admin”: [“myuser@”] to collect all devices belonging to a user, then write rules that permit specific traffic. For example, permitting all traffic between admin devices uses an entry in the acls array: {“action”: “accept”, “src”: [“group:admin”], “dst”: [“group:admin:“]}. Adding a rule that allows admin devices to reach any destination uses “dst”: [“:*”]. After editing the policy file, reload it without restarting the service by running headscale policy set –file /etc/headscale/policy.hujson. Changes propagate to clients within seconds.
DNS integration is another layer worth configuring. Headscale supports Magic DNS, the same feature that gives Tailscale devices short hostnames like device.tail-name.ts.net. In your Headscale config, set magic_dns: true and define a base domain under dns_config. Enrolled devices can then reach each other by hostname rather than IP address, which matters when IPs get reassigned after device re-enrollment. If you are already running a self-hosted password manager like Vaultwarden, enrolling that server in Headscale and accessing it by its Magic DNS hostname removes the need to expose it publicly at all – the entire service stays inside your private WireGuard mesh.

Keeping Headscale Running Long-Term
Headscale upgrades are a binary swap – stop the service, replace the binary, restart – but checking the release notes before each upgrade matters because the config file schema occasionally adds required fields or deprecates old ones. The SQLite database stores all device registrations and pre-auth keys, so including /var/lib/headscale/ in your backup routine is the only thing standing between you and re-enrolling every device from scratch after a disk failure.





