footer-logofooter-logo
Self-Hosting Headscale on a Dedicated Server: Step-by-Step

Self-Hosting Headscale on a Dedicated Server: Step-by-Step

Headscale gives you the half of Tailscale that isn't open source: the coordination server. Run it yourself and the official Tailscale clients — with their excellent NAT traversal and platform support — connect to infrastructure you control, with no per-user pricing and no third party holding your network's keys. This guide is the practical walkthrough: install from the official packages, TLS done the simple way, users and ACLs, the embedded DERP relay on your own network, and the honest operational limits of a deliberately small project.

12 juli 2026

door Jesse Schokker

Headscale

Tailscale

VPN

WireGuard

Loading...

Answer first: what you're building

One small server running Headscale — the open-source implementation of Tailscale's coordination server — with the official Tailscale apps on every device pointed at it instead of Tailscale's cloud. Same WireGuard mesh, same clients, same NAT traversal; the control plane (keys, peer lists, ACLs) lives on your hardware, for zero per-user cost. The build takes roughly half an hour and needs:

  • A small always-on Linux box with a public IP — Headscale's footprint is tiny; any modest server or VPS slice is ample.
  • A DNS name pointing at it (headscale.example.com — substitute yours throughout).
  • Ports: 443 (or your chosen port) for clients; 80 if you use the default Let's Encrypt flow; 3478/udp if you enable the embedded relay.

Worth restating from our mesh VPN comparison (where this option won the "Tailscale UX, self-hosted control" slot): Headscale is deliberately scoped to a single tailnet — one network, one team or org. It's the self-host answer for personal infrastructure and small organisations, not a multi-tenant Tailscale-business clone. Sized to that scope, it's excellent.

Step 1: install

The project's recommended path is its official .deb packages (Ubuntu 22.04+/Debian 12+), which set up the headscale user, config skeleton, and systemd unit for you. Grab the current release (v0.29.x at the time of writing — check releases):

wget https://github.com/juanfont/headscale/releases/download/v0.29.2/headscale_0.29.2_linux_amd64.deb
apt install ./headscale_0.29.2_linux_amd64.deb

(Docker images exist but the project explicitly doesn't officially support Docker deployment — on your own server, the package is both simpler and the supported path.)

Step 2: configure

Everything lives in /etc/headscale/config.yaml. The minimal working delta from the shipped example:

server_url: https://headscale.example.com:443
listen_addr: 0.0.0.0:443

# TLS, the simple way: built-in Let's Encrypt
tls_letsencrypt_hostname: headscale.example.com

Three decisions worth making consciously:

  • TLS: the built-in Let's Encrypt integration is the least-moving-parts option. The default HTTP-01 challenge needs port 80 reachable; alternatively TLS-ALPN-01 validates on 443 itself. A reverse proxy (nginx/Caddy) in front also works and is common — but note the docs' honest caveat that the reverse-proxy guidance is community-maintained, WebSocket upgrades must be configured, and proxying through Cloudflare is explicitly unsupported. Fewer layers, fewer surprises: direct TLS is the recommendation here.
  • Database: SQLite is the default and the right answer at this scope — one file at /var/lib/headscale/db.sqlite.
  • DNS: the dns: section gives your mesh MagicDNS names (base_domain: ts.example.com-style); set it now, renaming later is annoying.

Then:

systemctl enable --now headscale
headscale users create jesse

Step 3: connect clients

On each device, install the normal Tailscale app and point it at your server:

tailscale up --login-server https://headscale.example.com

The command prints a URL; visiting it gives you the node key, which you approve server-side (headscale nodes register --user jesse --key <key>). For unattended machines — servers joining the mesh — skip the interactive step with a pre-auth key:

headscale preauthkeys create --user jesse --expiration 1h
tailscale up --login-server https://headscale.example.com --authkey <key>

Headscale supports the last ten Tailscale client releases across Linux, Windows, macOS, iOS, and Android (mobile and desktop platforms have a small extra step to set a custom server — the docs cover each; your server even serves helper pages at /windows and /apple). Two devices in, run tailscale status and ping between them: you have a mesh.

For SSO instead of CLI user management, Headscale's OIDC support is solid — point it at Authentik, Keycloak, Entra ID, or Google (enable PKCE, per the docs' recommendation) and device registration becomes a browser login.

Step 4: ACLs

Out of the box every node reaches every node — fine for one person, not for a network with servers of different sensitivity. Policy is a HuJSON file (JSON with comments), same format as Tailscale's:

// /etc/headscale/policy.hujson
{
  "tagOwners": { "tag:server": ["jesse@"] },
  "acls": [
    // everyone reaches web things on servers
    { "action": "accept", "src": ["*"], "dst": ["tag:server:80,443"] },
    // only jesse reaches SSH anywhere
    { "action": "accept", "src": ["jesse@"], "dst": ["*:22"] }
  ]
}

Reference it via policy.path in the config and reload the service to apply changes. Notes from the current state of the project: the policy engine is v2 (groups, tags, autogroups like autogroup:internet for exit-node control), the newer grants syntax is where the project is heading, and one honest gap — OIDC groups can't be used in policy rules yet. Default-deny thinking applies here exactly as in any firewall: write the accepts you mean, nothing else.

Step 5: your own DERP relay (optional, worth it)

When two peers can't punch a direct connection (hard NAT both ends, UDP blocked), traffic falls back to a DERP relay. By default that means Tailscale's public relay fleet — functional, but the one moment your self-hosted mesh silently depends on someone else's infrastructure, at whatever latency their nearest node sits. Headscale embeds a DERP server; enabling it keeps relayed traffic on your own machine:

derp:
  server:
    enabled: true
    stun_listen_addr: "0.0.0.0:3478"

Open 3478/udp directly to the server (STUN can't ride a reverse proxy). Relayed packets are still end-to-end WireGuard-encrypted — DERP forwards ciphertext; it can't read anything — so the win here is latency and self-sufficiency, not confidentiality. On a well-peered network the difference is tangible: your worst-case path is a bounce through your own low-latency server rather than a detour through a distant public relay.

Operating it

  • Backups: the entire state is three things — /var/lib/headscale/ (database + noise key), /etc/headscale/ (config + policy). Snapshot or file-backup them; restoring onto a fresh box is copy-and-start.
  • Upgrades: read the changelog (the project is honest about breaking changes — v0.26 replaced the policy engine; v0.29 changed wildcard semantics), then install the new .deb and restart. Clients reconnect automatically.
  • Resources: the project publishes no sizing figures and doesn't need to — it comfortably coordinates hundreds of devices on minimal hardware. The one scaling note from the maintainers: cost is CPU-bound map recalculation when topology churns, so it's connection count and churn, not traffic, that matters — data flows peer-to-peer and never through the coordination server (except DERP fallback, above).
  • Visibility: there's deliberately no bundled web UI; the CLI covers administration, and third-party UIs (headscale-admin, Headplane and friends) exist as community projects — treat them as such, and note some require the policy in database mode.

The honest limits

Choose Headscale knowing what it isn't. Single tailnet — no multi-tenancy, by design and by FAQ. Community-run — it's an independent open-source reimplementation (with a Tailscale employee contributing in personal capacity, historically), not a vendor product with an SLA; new Tailscale features arrive when they're reimplemented, not on launch day. You're the operator — TLS renewals, upgrades, backups, and the 3 a.m. pager are yours. If those trade-offs read as costs rather than the point, Tailscale's hosted free tier (or NetBird, per our comparison) is the better-fitting tool. If they read as sovereignty, proceed — this is one of the most rewarding self-host projects there is: high utility, tiny operational surface.

Frequently asked questions

Is Headscale production-ready?

For its intended scope — a single organisation's tailnet run by someone comfortable operating one Linux service — yes: the project is mature, actively released, and widely deployed for exactly that. The caveats are scope, not quality: no multi-tenancy, no vendor support contract, and occasional breaking changes between minor versions that expect an operator who reads changelogs. Match it to a fleet of your own servers and devices and it's solid; stretch it toward "product for customers" and you're outside its design.

Why do my devices connect but traffic flows slowly through the server?

They've fallen back to DERP relaying — usually visible in tailscale status as a relay tag rather than a direct address. Direct WireGuard paths never touch your Headscale box, so "everything routes through the server" means NAT traversal failed (hard NAT, blocked UDP) and packets ride the relay. Fixes in order: ensure UDP isn't blocked at either end, enable the embedded DERP (so the fallback is at least your fast relay), and for permanently-relayed pairs check whether one side can get a direct port open — a dedicated server with open UDP makes an ideal always-direct anchor node.

Can I use my mesh to reach my servers' private/management network?

Yes — this is the operator use case the comparison article highlighted. Run one node inside the private segment as a subnet router (tailscale up --advertise-routes=10.0.0.0/24 ..., approve with headscale nodes approve-routes), and authorised mesh members reach the whole segment — IPMI, internal services — without any of it having a public listener. Gate the route behind a tight ACL and treat the subnet-router node with bastion-grade care; it's now the bridge into your most sensitive network.

How does this compare to just running WireGuard?

Headscale is WireGuard underneath — what you're adding is the control plane: automatic key distribution, NAT traversal, MagicDNS, ACLs, and mobile apps that non-experts can use. For two static servers, raw WireGuard stays simpler. The crossover comes with roaming devices, growing peer counts, or humans who deserve an app with a toggle — at which point Headscale delivers the product experience while keeping the raw-WireGuard property that mattered: nobody else's infrastructure in your control path.

Deploying on Serverside

A Headscale host wants exactly what a small dedicated server or cloud instance here provides: an always-on box with a stable public IP on a well-peered network — ASN 55285, where your embedded DERP relay actually earns its latency advantage — behind always-on DDoS mitigation, since your coordination server's one public listener deserves protection. Larger meshes anchoring subnet routers into private networking fit the dedicated line naturally.

The connected reading: the mesh VPN decision this guide follows through on, firewall rules for the host underneath, and the hardening checklist to run before any of it.

Artikel delen

Link delen

Jesse Schokker

Over de auteur

Jesse Schokker

Co-founder & CTO, Serverside.com

Jesse is the co-founder and CTO of Serverside.com, where he leads the engineering behind the company's bare-metal cloud — from the ASN 55285 backbone to sub-minute server provisioning. He writes about dedicated servers, operating systems, and running production workloads on bare metal.

Artikel delen

Link delen