Headscale Setup Guide: Self-Host Your Tailscale Control Server

Learn how to deploy Headscale with Docker on a VPS as a self-hosted Tailscale control server. Full mesh VPN with WireGuard, ACLs, and no vendor lock-in.

Headscale Setup Guide: Self-Host Your Tailscale Control Server

Tailscale makes mesh VPNs feel effortless. Install the client, log in, and every device on your account can reach every other device. No config files, no port forwarding, no VPN server to maintain. It’s built on WireGuard, which means fast, encrypted tunnels with minimal overhead.

The catch: Tailscale’s control server runs on their infrastructure. Your network topology, authentication data, and coordination plane all live on Tailscale’s servers. For most people that’s fine. But if you’re self-hosting because you want to own your infrastructure end-to-end — the same reason you’d switch from Cloudflare Tunnels to Pangolin — then Tailscale’s control plane is the one piece you still don’t control.

Headscale fixes that. It’s an open-source, self-hosted implementation of the Tailscale control server. You run it on your VPS, and standard Tailscale clients connect to it instead of Tailscale’s cloud. Same client experience, same WireGuard tunnels, but you hold the coordination server.

I’ve been running Headscale for my homelab for about six months. Here’s a complete setup guide.

Why self-host your Tailscale control server?

Why people switch to Headscale:

  • Data ownership: Tailscale’s coordination server knows every device on your network, its IP, its online status, and who can reach it. With Headscale, that data stays on your server.
  • No account dependency: Tailscale free tier gives you 6 user seats now, but if you need more or their pricing changes, you’re stuck. Headscale has no user limits — it’s your server.
  • Custom authentication: Tailscale uses their own identity provider. Headscale lets you plug in any OIDC provider — Authentik, Keycloak, Google, whatever you already run.
  • EU data sovereignty: If you need network coordination data to stay within a specific jurisdiction, Tailscale can’t guarantee that. Headscale on a Hetzner VPS in Falkenstein can.
  • No vendor lock-in: If Tailscale changes their terms, pricing, or features, you have no alternative. Headscale gives you a migration path.

Tailscale is still the better choice if you want zero-maintenance networking and don’t care about control plane ownership. Headscale is for people who want the Tailscale experience but need to own the whole stack.

Headscale vs Tailscale

FeatureHeadscaleTailscale
Control serverYour VPSTailscale’s cloud
Open sourceYes (BSD-3)Server is proprietary
User limitUnlimited6 (free tier)
ClientTailscale clients (unofficial)Tailscale clients (official)
AuthenticationAny OIDC providerTailscale’s identity
MagicDNSBasicFull implementation
ACLsYes (JSON config)Yes (web UI)
Exit nodesYesYes
Funnel (public endpoints)NoYes
DERP relaysCustom or Tailscale’sTailscale’s global network
CostVPS ($3-5/month)Free tier / paid plans
Setup complexityModerateNear-zero

Unofficial client support

Headscale is not made by Tailscale. Tailscale clients work with it, but this is unofficial. Tailscale could change their client protocol at any point. In practice, this hasn’t caused problems, but it’s worth knowing before you commit.

What you need

Before setting up Headscale:

  • A VPS with a public IP (Ubuntu 22.04+ or Debian 12+). Hetzner or Hostinger work well.
  • A domain name pointing to your VPS (for the control server and optionally a web UI)
  • Docker and Docker Compose installed on the VPS
  • Port 443 (TCP) open for the control server, and optionally port 3478 (UDP) for DERP relay
  • Tailscale clients installed on the devices you want to connect

Install Headscale with Docker

The simplest way to run Headscale is with Docker Compose. This gives you the control server plus optional web management UIs.

Step 1: Create the directory structure

mkdir -p /opt/headscale/{config,data}
cd /opt/headscale

Step 2: Download the config file

wget -O config/config.yaml \
  https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml

Step 3: Edit the configuration

Open config/config.yaml and adjust these key settings:

# The URL your clients will connect to
server_url: https://headscale.example.com

# Listen address (inside container, 0.0.0.0)
listen_addr: 0.0.0.0:8080

# Metrics endpoint
metrics_listen_addr: 0.0.0.0:9090

# SQLite database path (inside container)
database:
  type: sqlite
  sqlite:
    path: /var/lib/headscale/db.sqlite

# DERP configuration (use Tailscale's relays or set up your own)
derp:
  urls:
    - https://controlplane.tailscale.com/derpmap/default

# DNS configuration
dns_config:
  base_domain: example.com

# Random key for WireGuard (generate one)
private_key_path: /var/lib/headscale/private.key

Generate a WireGuard private key:

docker run --rm docker.io/headscale/headscale:0.24 \
  headscale generatekey

Save the output key and add it to your config or let Headscale auto-generate it on first run.

Step 4: Create the Docker Compose file

Create /opt/headscale/docker-compose.yml:

services:
  headscale:
    image: docker.io/headscale/headscale:0.24
    restart: unless-stopped
    container_name: headscale
    command: serve
    read_only: true
    tmpfs:
      - /var/run/headscale
    volumes:
      - ./config:/etc/headscale:ro
      - ./data:/var/lib/headscale
    ports:
      - "127.0.0.1:8080:8080"
      - "127.0.0.1:9090:9090"
    healthcheck:
      test: ["CMD", "headscale", "health"]
      interval: 30s
      timeout: 10s
      retries: 3

  headplane:
    image: ghcr.io/tale/headplane:0.3
    restart: unless-stopped
    container_name: headplane
    volumes:
      - ./config:/etc/headscale
      - ./data:/var/lib/headscale
    ports:
      - "127.0.0.1:3003:3003"
    environment:
      - HEADSCALE_URL=http://headscale:8080

Headplane is a lightweight web UI for managing Headscale. It lets you view users, devices, and routes without SSH-ing into the server every time.

Step 5: Start the containers

docker compose up -d

Verify Headscale is running:

docker compose exec headscale headscale health

You should see output confirming the server is healthy.

Step 6: Set up the reverse proxy

You need a reverse proxy to expose Headscale over HTTPS. If you’re already running Traefik or Caddy, add Headscale as a backend. Here’s a quick Caddy setup:

# Install Caddy
apt install caddy

# Edit /etc/caddy/Caddyfile
headscale.example.com {
    reverse_proxy localhost:8080
}

headplane.example.com {
    reverse_proxy localhost:3003
}

Reload Caddy:

systemctl reload caddy

Caddy automatically provisions Let’s Encrypt certificates for both domains.

Create your first user and register devices

Headscale organizes devices into “users” (similar to Tailscale’s tailnets). Each user gets their own private network.

Create a user

docker compose exec headscale headscale users create myuser

Register a device

On each device you want to connect, first configure the Tailscale client to use your Headscale server instead of Tailscale’s:

Alternatively, pre-register a key from the server and use it on the client:

# On the server: create a pre-auth key
docker compose exec headscale headscale preauthkeys create \
  --user myuser --reusable

# On the client: use the key
tailscale up --login-server=https://headscale.example.com --authkey=tskey-xxxxx

This is the preferred method for servers and automated setups.

Verify the connection

Check registered devices:

docker compose exec headscale headscale nodes list

From any connected device, verify you can reach others:

tailscale status
tailscale ping other-device-name

Configure ACLs (Access Control Lists)

ACLs define which devices can reach which other devices. In Tailscale, you configure them through the web UI. In Headscale, you write them as JSON.

Create /opt/headscale/config/acls.json:

{
  "groups": {
    "group:admin": ["myuser@example.com"],
    "group:devices": ["myuser@example.com"]
  },
  "acls": [
    {
      "action": "accept",
      "src": ["group:admin"],
      "dst": ["*:*"]
    },
    {
      "action": "accept",
      "src": ["group:devices"],
      "dst": ["group:devices:*"]
    }
  ]
}

This gives admin users access to everything and device users access to other devices in their group. Update your config.yaml to reference the ACL file:

acl_file_path: /etc/headscale/acls.json

Restart Headscale to load the ACLs:

docker compose restart headscale

Set up exit nodes

Exit nodes let you route all your traffic through a specific device — like a VPN tunnel to your home network. Any device on your Headscale network can be an exit node.

On the device that will serve as the exit node:

tailscale up --login-server=https://headscale.example.com --advertise-exit-node

Approve the exit node on the server:

docker compose exec headscale headscale nodes approve-routes \
  --node <node-id> --routes 0.0.0.0/0

On other devices, use the exit node:

tailscale up --login-server=https://headscale.example.com --exit-node=<exit-node-name>

All traffic now routes through the exit node device.

Optional: Connect an OIDC identity provider

Headscale supports any OIDC-compatible identity provider. If you run Authentik, Keycloak, or Google Workspace, you can use it for authentication instead of Headscale’s built-in auth.

In your config.yaml:

oidc:
  issuer: "https://authentik.example.com/application/o/headscale/"
  client_id: "your-client-id"
  client_secret: "your-client-secret"
  scope: ["openid", "profile", "email"]
  allowed_domains:
    - example.com

Users authenticate through your identity provider when they run tailscale up. The email domain filter ensures only authorized users can join.

Optional: Set up custom DERP relays

DERP relays handle traffic when direct WireGuard connections can’t be established (both devices behind NAT, different CGNAT ISPs). Tailscale runs a global DERP network. Headscale defaults to using it, but you can add your own relay for reliability or latency reasons.

Add your custom DERP server to the Headscale config:

derp:
  urls:
    - https://controlplane.tailscale.com/derpmap/default
  paths:
    - /etc/headscale/derp.yaml
  auto_update: true

Create /opt/headscale/config/derp.yaml with your relay config. The Tailscale DERP server is open source and can be self-hosted.

How it compares to other self-hosted mesh VPNs

For a detailed comparison, see our mesh VPN comparison guide covering NetBird, Headscale, and Tailscale side by side. Here’s a quick summary:

  • Headscale: Best if you already know Tailscale and want the same client experience with self-hosted control. Requires understanding of Tailscale’s model.
  • NetBird: Best if you want a fully self-hosted mesh VPN with a clean web UI, built-in SSO, and no dependency on Tailscale clients. See our NetBird vs Headscale vs Tailscale comparison.
  • Pangolin: Best if you want to expose specific services through a reverse proxy tunnel, not a full mesh VPN. See our Pangolin setup guide.

Performance and reliability in practice

After six months of running Headscale on a Hetzner CX22 ($5/month):

The good stuff: devices connect and authenticate within seconds. WireGuard tunnels between devices on the same ISP achieve near-native speeds (80-100 Mbps on a 100 Mbps connection). Cross-ISP connections route through DERP relays with expected latency overhead. ACLs work reliably once you get the JSON format right. The Headplane web UI is simple but functional for day-to-day management.

The tradeoffs: you lose MagicDNS’s full feature set — Headscale supports basic DNS but not the name resolution that Tailscale provides out of the box. Tailscale Funnel (exposing public endpoints) doesn’t work with Headscale. You manage configuration through YAML files rather than a polished web UI. DERP relay performance depends on where you deploy — Tailscale’s global relay network is hard to beat with a single VPS.

Should you self-host your mesh VPN?

Headscale is worth setting up if you care about data ownership, need custom authentication, or want to avoid Tailscale’s user limits. The setup takes 30-45 minutes, and after that, adding devices is the same experience as regular Tailscale.

If you just want your devices to talk to each other with minimal effort, Tailscale’s managed service is still the better choice. Headscale adds maintenance overhead (updates, config changes, DERP relay management) that you don’t get with Tailscale’s cloud.

If you’re already self-hosting other infrastructure — running your own PaaS with Coolify, managing services with Docker on your home server — then Headscale fits naturally into that stack. The maintenance cost is small compared to the control you gain.

The Headscale GitHub repo has detailed docs, and the community is active on their Discord server.

FAQ

Can I migrate from Tailscale to Headscale?

Yes, but it requires re-registering every device. There’s no automatic migration. You’ll need to tailscale down each device, then tailscale up --login-server=https://headscale.example.com to re-register it with your Headscale server. Plan the migration when you can tolerate a brief network disruption.

Does Headscale work with the official Tailscale Android/iOS apps?

Not directly. The mobile apps don’t support changing the login server through the GUI. You need to use alternative approaches: on Android, you can change the login server through the app’s debug settings. On iOS, it’s more complicated and may require a custom build. This is one area where Headscale is less convenient than Tailscale.

Can I run Headscale and Tailscale simultaneously?

No on the same device. A Tailscale client connects to one control server at a time. But you can run some devices on Headscale and others on Tailscale — they’ll be on separate networks and won’t see each other.

What happens if my Headscale VPS goes down?

Existing WireGuard tunnels between already-connected devices stay alive. The control server is only needed for new connections, key rotations, and ACL changes. But devices can’t authenticate new peers or update their network maps until the server comes back. Run monitoring with Uptime Kuma or Beszel to catch downtime fast.