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:

AspectHosted TailscaleSelf-hosted Headscale
Control planeTailscale’s cloudYour own VPS
Node keys and identityHeld by TailscaleHeld by you, on your disk
Account-level device quotaPlan tiers applyNo quota in Headscale itself
LoginEmail / SSO / OIDC built inPre-auth keys, or your own OIDC provider (Authentik, Keycloak, Google)
serve and funnelIncludedHosted-only features — not in Headscale
Your responsibilitiesNone beyond the clientTLS, upgrades, backups, access policy
Device clientOfficial Tailscale clientThe same official Tailscale client
Data planeWireGuard peer-to-peerWireGuard 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:

  1. Provision a VPS with a public IP — Ubuntu 22.04+ or Debian 12+ is the tested lane.
  2. Point a domain at it — the control server needs a real hostname for HTTPS on 443.
  3. Install the official .deb from GitHub releases, or the Docker Compose stack above.
  4. Set server_url in config.yaml to the exact public HTTPS URL — the #1 install-breaker.
  5. Start Headscale and verify — headscale version and systemctl is-active headscale.
  6. Create your first user with headscale users create.
  7. Mint a pre-auth key — reusable for automation, single-use with 24h expiry for humans.
  8. Join the first node — tailscale logout first if it was on hosted Tailscale, then tailscale up --login-server ... --authkey ....
  9. Confirm registration with headscale list nodes on the server.
  10. Add an ACL policy file, point policy.path at 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:

MistakeWhy it hurtsThe fix
Leaving server_url at the localhost defaultNo client can ever reach the serverSet the public HTTPS URL before the first node joins
Hiding Headscale behind a Cloudflare tunnelThe control plane does not work through itGive the server a real public IP on a VPS
Pasting a single-use key into a fleet scriptFirst node joins, every other join is rejectedUse --reusable keys for automation
Joining a machine still logged into hosted TailscaleRegistration conflict with the old control planeRun tailscale logout before joining
Running the Tailscale client on the Headscale serverUnsupported per the Headscale FAQ; breaks routers, relays, MagicDNSRun the exit node on a second server
Forgetting IP forwarding on the exit nodeRoutes approve fine, then packets die on the nodeEnable net.ipv4.ip_forward before advertising
Copying stale CLI flags from old guidesThe CLI syntax evolved; old flags do not existRun 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.

Featured on Twelve Tools Launch Llama Tools Launch your MCP