How to Use Headscale: The Beginner's Guide to a Self-Hosted Tailscale Control Server
CodexFuse Team · 2026-10-31T20:41:00.000Z · 14 min read min read
Install Headscale on a VPS, set server_url right, mint pre-auth keys, join your first node, write an ACL policy, and approve your first exit node.
Learning how to use Headscale is learning how to own your private network instead of renting it. Headscale is an open-source, self-hosted implementation of the Tailscale control server — the coordination piece that registers your devices, swaps their WireGuard keys, and hands out tailnet addresses. The official Tailscale clients still run on every device; they just point at your server instead of Tailscale’s cloud. This Headscale tutorial is the complete Headscale for beginners arc: what Headscale actually replaces, how it differs from hosted Tailscale, installing it on a VPS, the server_url rule that breaks most first installs, your first user and pre-auth key, joining your first node, the Docker route, writing an ACL policy, turning a node into an exit node, and the community web UI. The current release is v0.29.7 (October 30, 2026), the project carries roughly 45,230 GitHub stars under the BSD-3-Clause license, and a running server uses about 15 MB of RAM with a couple of nodes connected. If you have never run the client side at all, our Tailscale beginner guide explains it first — this guide owns the self hosted tailscale control lane.
What is Headscale, exactly?
Headscale is a self-hosted control server that manages a Tailscale mesh network without Tailscale’s cloud. Your devices keep running the official Tailscale client — Headscale does not replace the client — but registration, key exchange, tailnet IP assignment, DNS, and route settings all flow through your machine. It was created by Juan Font Alvarez, it is written in Go, it carries the BSD-3-Clause license, and the repository dates back to June 2020. Two honest caveats up front: it is a community project, not made by Tailscale, so a future client-protocol change could break it; and actual traffic between your devices stays peer-to-peer WireGuard — the server only coordinates, it never carries your data except through the embedded DERP relay when NAT blocks the direct path.
Headscale vs Tailscale: what actually changes?
Headscale vs Tailscale is a control-plane question, not a VPN-protocol question. The tunnels are the same WireGuard tunnels; what changes is who holds the keys and who does the bookkeeping:
| Aspect | Hosted Tailscale | Self-hosted Headscale |
|---|---|---|
| Control plane | Tailscale’s cloud | Your own VPS |
| Node keys and identity | Held by Tailscale | Held by you, on your disk |
| Account-level device quota | Plan tiers apply | No quota in Headscale itself |
| Login | Email / SSO / OIDC built in | Pre-auth keys, or your own OIDC provider (Authentik, Keycloak, Google) |
| serve and funnel | Included | Hosted-only features — not in Headscale |
| Your responsibilities | None beyond the client | TLS, upgrades, backups, access policy |
| Device client | Official Tailscale client | The same official Tailscale client |
| Data plane | WireGuard peer-to-peer | WireGuard peer-to-peer — unchanged |
The decision rule is simple: choose Headscale when you want the coordination server on infrastructure you control and are willing to operate it — TLS certificates, upgrades, backups, and the access policy become your jobs. Choose hosted Tailscale when you want zero server work and are comfortable with Tailscale holding the keys.
What do you need before you install Headscale?
You need three things, and the first two are non-negotiable. First, a VPS with a public IP address — the control server must be reachable from every network your devices roam onto, so a box behind your home NAT will not do; if you still need the machine itself, read what is VPS hosting first. Second, a domain name pointed at that VPS, because the Tailscale client expects HTTPS on port 443 in several places and the setup strongly favors a real hostname over a bare IP. Third, basic comfort with a Linux terminal. One popular shortcut does not work here: Headscale does not run behind a Cloudflare tunnel, so do not plan to hide the server behind one — a public IP is required.
How do you install Headscale on a VPS?
The headscale install beginners should use is the official .deb package from GitHub releases. It is the method upstream recommends, because the package creates the headscale system user, the data directory, and the systemd unit for you. The same install block keeps working across releases, because the first command asks the GitHub releases API for the latest tag:
sudo apt update && sudo apt install -y curl wget
export HS_VER=$(curl -sL https://api.github.com/repos/juanfont/headscale/releases/latest | grep tag_name | head -1 | sed 's/.*"v//;s/".*//')
wget --output-document=headscale.deb "https://github.com/juanfont/headscale/releases/download/v${HS_VER}/headscale_${HS_VER}_linux_$(dpkg --print-architecture).deb"
sudo apt install -y ./headscale.deb
headscale version
systemctl is-active headscale
Note what the package does on first boot: it starts with the default configuration, which listens only on localhost, so nothing is exposed to the internet yet — that is safe by default, and the next section fixes it deliberately. The current line is v0.29.7, released October 30, 2026, and in lab testing the October 2026 guide measured the running server at just 15.4 MB of RAM after an hour with two nodes connected and a policy loaded — the smallest VPS you can buy is plenty.
Why does server_url break most first installs?
Because the default is a placeholder: the example configuration ships with server_url: http://127.0.0.1:8080, and every headscale login server join fails if you never change it. The rule is that server_url must be the exact public HTTPS address your clients will reach — for example https://headscale.example.com — and the --login-server flag on every device must match it character for character. The configuration comments confirm two related rules: the embedded DERP relay only works once TLS is in place, and the magic DNS base domain must be a fully qualified name different from the server URL’s domain. Practically, most people terminate TLS on nginx or Caddy on port 443 and proxy to Headscale on localhost, keeping the tiny control plane itself off the public internet.
How do you create your first user and a pre-auth key?
Headscale has no email login; instead you create named users and mint keys for them from the server CLI. Two commands, in this order:
sudo headscale users create alice
sudo headscale preauthkeys create --user alice --reusable --expiration 24h
The headscale pre-auth key is a password by another name, so treat it like one: never publish it in screenshots, shell history, or repositories. There are two flavors for two jobs. A reusable key with a long expiration is for automation — baking it into a fleet script or a cloud-init template that joins dozens of machines. A single-use key with the 24-hour default expiration is for humans joining one device by hand. Picking the wrong flavor is a classic beginner failure: a single-use key pasted into an install script joins the first machine and silently rejects the rest.
How do you join your first node to Headscale?
Install the official Tailscale client on the device, then point it at your server with the key you just minted:
sudo tailscale up --login-server https://headscale.example.com --authkey YOUR_PREAUTH_KEY
Back on the server, sudo headscale list nodes confirms the node registered. Two lessons from real installs: if the machine was previously on hosted Tailscale, run tailscale logout first — joining while still registered to the old control plane is a conflict, not a migration. And per-OS joins differ in where the URL goes: Linux uses the --login-server flag shown above, while the desktop and mobile clients point at the server through their alternate-control-server setting — the Windows registry LoginURL path the community guides document is the classic example — before you sign in.
How do you run Headscale with Docker instead?
The headscale docker route uses the official headscale/headscale image with the configuration mounted from the host. The server_url rule applies identically — containerization does not excuse a localhost URL:
services:
headscale:
image: headscale/headscale:latest
container_name: headscale
volumes:
- ./config:/etc/headscale
- ./data:/var/lib/headscale
ports:
- "8080:8080"
- "3478:3478/udp"
restart: unless-stopped
command: headscale serve
Put your edited config.yaml — with server_url set to the public HTTPS address — in ./config before the first boot, terminate TLS in front of the container on port 443, and give the container the UDP port the embedded DERP relay wants. If Docker itself is new to you, our Docker beginner guide covers Compose first; if you would rather have automatic HTTPS than hand-rolled nginx certificates, our Caddy beginner guide is the TLS lane. After the container is up, the user, key, and node commands run the same way — either by executing into the container or by pointing a local headscale CLI at the same config and database.
How do you write an ACL policy for Headscale?
Headscale accepts Tailscale-format access policies, so the headscale acl workflow is: write a policy file, point the server at it, restart. The example configuration’s policy section offers two modes — file, reading a HuJSON file from path, or database — and it links Tailscale’s own access-control documentation as the format reference, which is why you should cite those docs rather than inventing policy syntax. The documented default, which allows every member to reach every member, looks like this:
{
"tagOwners": {},
"acls": [
{ "action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:member:*"] }
]
}
Save that as a .hujson file — the format tolerates comments and trailing commas — set policy.path to its location, and restart Headscale. For anything beyond the default, write your real policy against Tailscale’s access-control docs and the newer grants format the same docs describe; Headscale mirrors the semantics, so a policy you understand on hosted Tailscale ports over directly. The operational lesson: tighten the policy the week you install, not the year you get compromised.
How do you turn a node into an exit node?
An exit node routes a device’s internet traffic out through another node on your tailnet — the headscale exit node setup is a deliberate double opt-in, once on the node and once on the server. First, on the machine that will serve as the exit, enable IP forwarding, because without it the routes get approved and packets still die there:
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p
sudo tailscale up --login-server https://headscale.example.com --advertise-exit-node
Then, on the Headscale server, list what the node advertised and approve it:
sudo headscale nodes list-routes
sudo headscale nodes approve-routes --identifier 1 --routes 0.0.0.0/0
An exit node advertises itself as the two default routes; approving either the IPv4 or IPv6 route approves the other automatically, and the server answers Node updated. The double opt-in is a security feature, not bureaucracy: a compromised node could otherwise announce a default route and pull every client’s traffic through itself. Two final rules from the community guides: do not run the exit node on the Headscale server itself — the Headscale FAQ calls running the client and server on one machine unsupported — use a second server; and on each client, select the exit node explicitly (the Windows tray menu, the macOS menu bar, the Exit Node section of the iPhone and Android apps, or tailscale set --exit-node=<IP> on Linux). For fleets, the policy’s autoApprovers section approves tagged exit nodes automatically.
How do you manage Headscale with a web UI?
Headscale itself is CLI-only, so the headscale ui answer is the community’s headscale-ui project, which connects with an API key you generate on the server. Create the key with an expiration that matches your rotation habits:
sudo headscale apikeys create --expiration 90d
Enter that key in the UI’s settings page, and you get a dashboard for users, connected nodes, route approvals, ACL inspection, and key generation — the clicking-around layer over the same commands this guide teaches. Treat the UI as a convenience, not the source of truth: it is a community project, and the CLI remains the documented interface for anything the UI does not cover.
Your first working tailnet in 10 steps
The whole headscale install arc, end to end, in the order you actually run it:
- Provision a VPS with a public IP — Ubuntu 22.04+ or Debian 12+ is the tested lane.
- Point a domain at it — the control server needs a real hostname for HTTPS on 443.
- Install the official .deb from GitHub releases, or the Docker Compose stack above.
- Set
server_urlinconfig.yamlto the exact public HTTPS URL — the #1 install-breaker. - Start Headscale and verify —
headscale versionandsystemctl is-active headscale. - Create your first user with
headscale users create. - Mint a pre-auth key — reusable for automation, single-use with 24h expiry for humans.
- Join the first node —
tailscale logoutfirst if it was on hosted Tailscale, thentailscale up --login-server ... --authkey .... - Confirm registration with
headscale list nodeson the server. - Add an ACL policy file, point
policy.pathat it, and restart — your tailnet is now real.
7 beginner mistakes that break a Headscale install
Read this table once and skip the debugging session most beginners pay for in their first week:
| Mistake | Why it hurts | The fix |
|---|---|---|
Leaving server_url at the localhost default | No client can ever reach the server | Set the public HTTPS URL before the first node joins |
| Hiding Headscale behind a Cloudflare tunnel | The control plane does not work through it | Give the server a real public IP on a VPS |
| Pasting a single-use key into a fleet script | First node joins, every other join is rejected | Use --reusable keys for automation |
| Joining a machine still logged into hosted Tailscale | Registration conflict with the old control plane | Run tailscale logout before joining |
| Running the Tailscale client on the Headscale server | Unsupported per the Headscale FAQ; breaks routers, relays, MagicDNS | Run the exit node on a second server |
| Forgetting IP forwarding on the exit node | Routes approve fine, then packets die on the node | Enable net.ipv4.ip_forward before advertising |
| Copying stale CLI flags from old guides | The CLI syntax evolved; old flags do not exist | Run headscale <command> --help on your installed release |
Sources
Facts in this guide were verified at write time (November 1, 2026) from: the GitHub juanfont/headscale repository and releases APIs (latest tag v0.29.7, published 2026-10-30; roughly 45,230 stars; BSD-3-Clause license; repository created 2020-06-21 — fetched November 1, 2026); the project’s current config-example.yaml on the main branch (default server_url, the policy section with file/database modes and the path field, the link to Tailscale’s access-control docs, and the TLS note for the embedded DERP relay — fetched November 1, 2026); ComputingForGeeks’ “Headscale Setup on Ubuntu: Self-Hosted Tailscale” (15 days old — official .deb install from GitHub releases, localhost-only default config, 15.4 MB RAM with two nodes and a policy after an hour, Headscale 0.29.5 with Tailscale 1.104.1 tested in October 2026, nginx with Let’s Encrypt, grants policy and MagicDNS, the exit-node double opt-in with headscale nodes list-routes and headscale nodes approve-routes, IP-forwarding lesson, and --accept-routes on clients); arct.cloud’s “Headscale: Self-Host a Tailscale Control Server” (21 days old, checked October 2026 — BSD-3-Clause, registration/key-exchange/address-assignment role, the Headscale FAQ line that running the Tailscale client on the Headscale server is unsupported, and per-platform exit-node selection); massivegrid’s “Self-Host Headscale on Ubuntu VPS” (54 days old — exit-node and subnet-router setup, headscale routes list / routes enable, headscale apikeys create --expiration 90d for the web UI, and the Windows registry LoginURL lesson); the official headscale.net routes documentation (exit-node setup, recognizing an exit node by the 0.0.0.0/0 and ::/0 routes, approving either IPv4 or IPv6 approves both, and the autoApprovers policy section); bitdoze’s “Headscale Setup Guide” (164 days old — Docker Compose install path, server_url in config.yaml, the VPS/domain/ports requirements, the unofficial-project warning, and plugging in any OIDC provider); the dev.to step-by-step Headscale guide (464 days old — install arc, headscale users create, headscale preauthkeys create --reusable --expiration 24h --user, tailscale up --login-server ... --authkey, headscale list nodes); MakeUseOf’s Headscale piece (100 days old — the lesson that Headscale does not work behind a Cloudflare tunnel); ssdnodes’ Headscale guide (19 days old — pre-auth key vs auth-register forms, the exit-node approval lesson, and serve/funnel as hosted-only features); localboxmail.co.za’s “Headscale and Tailscale VPN Basics” (your operating responsibilities when self-hosting, treating auth keys like passwords, and using --help on the installed release); and the Docker Hub API (the official headscale/headscale image — fetched November 1, 2026). This run’s live search for “how to use headscale tutorial” returned guide and tutorial articles only — zero tool pages, no reroute.
Frequently Asked Questions
Is Headscale made by Tailscale?
No. Headscale is an independent open-source reimplementation of the Tailscale control server, and Tailscale has never endorsed it. That is why every guide worth reading carries the same warning: a future change to the client protocol could break compatibility, so keep the server updated and watch the release notes.
Can I run Headscale on a home server behind NAT?
Not reliably. The control server must be reachable from every network your devices roam onto, and a home NAT breaks that — this is also why hiding it behind a Cloudflare tunnel does not work. A small VPS with a public IP is the intended home for Headscale.
How do pre-auth keys differ from logging in with hosted Tailscale?
Hosted Tailscale authenticates you through email, SSO, or OIDC in a browser. Headscale has no browser login at all: you mint pre-auth keys on the server CLI and hand one to each joining device. Single-use keys with a 24-hour default expiration fit humans; reusable keys fit automation and fleet scripts.
Is there a device limit on Headscale?
Headscale itself imposes no account-level device quota — the ceiling is your server’s CPU and memory. The running server is tiny: lab testing measured about 15.4 MB of RAM after an hour with two nodes connected and a policy loaded, so even the smallest VPS comfortably hosts a personal or small-team tailnet.
What do I lose by switching from hosted Tailscale to Headscale?
The hosted-only features: serve and funnel are not part of Headscale, and you take on TLS, upgrades, backups, and the access policy yourself. What you gain is full ownership of the node keys and identity, the freedom to plug in any OIDC provider, and no per-account device quota — the clients on your devices stay the official ones.