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.
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
| Feature | Headscale | Tailscale |
|---|---|---|
| Control server | Your VPS | Tailscale’s cloud |
| Open source | Yes (BSD-3) | Server is proprietary |
| User limit | Unlimited | 6 (free tier) |
| Client | Tailscale clients (unofficial) | Tailscale clients (official) |
| Authentication | Any OIDC provider | Tailscale’s identity |
| MagicDNS | Basic | Full implementation |
| ACLs | Yes (JSON config) | Yes (web UI) |
| Exit nodes | Yes | Yes |
| Funnel (public endpoints) | No | Yes |
| DERP relays | Custom or Tailscale’s | Tailscale’s global network |
| Cost | VPS ($3-5/month) | Free tier / paid plans |
| Setup complexity | Moderate | Near-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:
# Install Tailscale client
curl -fsSL https://tailscale.com/install.sh | sh
# Point it to your Headscale server
tailscale up --login-server=https://headscale.example.comThis prints a registration URL. Open it in your browser to complete the auth.
Install Tailscale from the Mac App Store, then:
/Applications/Tailscale.app/Contents/MacOS/Tailscale up \
--login-server=https://headscale.example.com Install Tailscale for Windows. Then edit the login server in the Tailscale GUI settings, or run from Command Prompt:
tailscale up --login-server=https://headscale.example.com 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.