How to Self-Host Headscale: Tailscale Alternative for Private VPN Mesh 2026
TL;DR
Headscale is an open source implementation of the Tailscale control server — BSD 3-Clause, ~22K GitHub stars, written in Go. You run your own coordination server; Tailscale clients connect to it instead of Tailscale's cloud. Result: a private WireGuard mesh network you fully control — same ease-of-use as Tailscale, no data sent to Tailscale's servers. Deploy Headscale on any VPS in 10 minutes.
Key Takeaways
- Headscale: BSD 3-Clause, ~22K stars, Go — open source Tailscale control server
- Use Tailscale clients: Headscale is only the control plane; all clients remain standard Tailscale apps
- WireGuard mesh: All nodes connect directly peer-to-peer (no traffic through your server)
- Privacy: Your node list, keys, and ACLs stay on your server — not Tailscale's cloud
- Cost: Free for unlimited devices (vs Tailscale Free: 3 users / 100 devices, paid plans $6+/user/mo)
- Subnet routing and exit nodes: Full Tailscale features work with Headscale
How Tailscale/Headscale Works
Traditional VPN: Client → VPN Server → Internet (all traffic through server)
Tailscale mesh: Client ←→ Control Server (key exchange only)
Client ←→ Client (direct WireGuard connection)
The control server (Tailscale's cloud, or your Headscale) only handles:
- Node registration and authentication
- Key distribution (WireGuard public keys)
- ACL policy distribution
Actual traffic flows directly between nodes (peer-to-peer), not through the control server.
Headscale vs Tailscale Free vs ZeroTier
| Feature | Headscale | Tailscale Free | Tailscale Business | ZeroTier |
|---|---|---|---|---|
| Cost | VPS only | Free (limits) | $6/user/month | Free (25 devices) |
| Max devices | Unlimited | 100 devices | Unlimited | 25 (free) |
| Users | Unlimited | 3 users | Per plan | Unlimited |
| Control | Full (self-hosted) | Tailscale's cloud | Tailscale's cloud | ZeroTier cloud |
| iOS/Android app | Tailscale app | Tailscale app | Tailscale app | ZeroTier app |
| Exit nodes | ✅ | ✅ | ✅ | ✅ |
| Subnet routing | ✅ | ✅ | ✅ | ✅ |
| MagicDNS | Via DNS config | ✅ | ✅ | Via DNS |
| Open source | ✅ | Client only | Client only | ✅ |
Part 1: Deploy Headscale
# docker-compose.yml
version: '3.8'
services:
headscale:
image: headscale/headscale:latest
container_name: headscale
restart: unless-stopped
ports:
- "8080:8080"
- "9090:9090" # Metrics (optional)
volumes:
- ./config:/etc/headscale
- ./data:/var/lib/headscale
command: headscale serve
Configuration File
# config/config.yaml
server_url: https://headscale.yourdomain.com:443
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090
# Database
db_type: sqlite3
db_path: /var/lib/headscale/db.sqlite
# Private key for the server
private_key_path: /var/lib/headscale/private.key
# Noise protocol (Tailscale's encryption layer)
noise:
private_key_path: /var/lib/headscale/noise_private.key
# IP address allocation for nodes
ip_prefixes:
- 100.64.0.0/10 # Standard Tailscale IP range
- fd7a:115c:a1e0::/48
# DNS
dns_config:
magic_dns: true
base_domain: your-tailnet.net # e.g., nodes get: hostname.your-tailnet.net
nameservers:
- 1.1.1.1
# DERP relay servers (Tailscale's relay infrastructure for NAT traversal)
# Use Tailscale's DERP or run your own
derp:
server:
enabled: false # Disable built-in DERP (use Tailscale's public DERP)
urls:
- https://controlplane.tailscale.com/derpmap/default
auto_update_enabled: true
update_frequency: 24h
# Logging
log:
level: info
format: text
# ACME (TLS certificate) — if you want Headscale to handle TLS directly
acme_url: https://acme-v02.api.letsencrypt.org/directory
acme_email: admin@yourdomain.com
tls_letsencrypt_hostname: headscale.yourdomain.com
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_challenge_type: TLS-ALPN-01
# Start Headscale:
docker compose up -d
# Check it's running:
docker exec headscale headscale version
Part 2: HTTPS with Caddy
For Headscale behind a reverse proxy (recommended instead of built-in ACME):
headscale.yourdomain.com {
reverse_proxy localhost:8080
# gRPC support (required by Tailscale clients):
transport http {
versions h2c 2
}
}
In config.yaml, set:
server_url: https://headscale.yourdomain.com
tls_letsencrypt_hostname: "" # Leave empty (Caddy handles TLS)
Part 3: Create Users and Register Nodes
Create a User (Headscale namespace)
# Create a user:
docker exec headscale headscale users create alice
# List users:
docker exec headscale headscale users list
Generate Auth Key
# Create a reusable auth key for a user:
docker exec headscale headscale preauthkeys create --user alice --reusable --expiration 24h
# Output:
# Key: 1234567890abcdef...
Register a Node (on the client device)
Install the Tailscale client on any device, then:
# macOS / Linux:
tailscale up --login-server https://headscale.yourdomain.com --authkey 1234567890abcdef
# Or register interactively (browser-based):
tailscale up --login-server https://headscale.yourdomain.com
# Open the shown URL in browser → log in → Headscale registers the node
On Headscale server, approve the registration:
# List pending nodes:
docker exec headscale headscale nodes list
# Approve a specific node (if not using auth keys):
docker exec headscale headscale nodes register --user alice --key nodekey:abc123
View Connected Nodes
docker exec headscale headscale nodes list
# ID Hostname User IP Addresses Last Seen
# 1 alice-macbook alice 100.64.0.1, fd7a:... 2026-03-09 10:00:00
# 2 home-server alice 100.64.0.2, fd7a:... 2026-03-09 10:00:01
# 3 work-laptop alice 100.64.0.3, fd7a:... Online
Test Connectivity
From any registered node:
# Ping another node by Tailscale IP:
ping 100.64.0.2
# Or by MagicDNS hostname:
ping home-server.your-tailnet.net
# SSH directly:
ssh 100.64.0.2
Part 4: Access Control Lists (ACLs)
Headscale supports Tailscale's ACL format. ACLs control which nodes can communicate:
# config/acls.hujson
{
"groups": {
"group:admin": ["alice"],
"group:devs": ["alice", "bob"],
"group:readonly": ["charlie"]
},
"acls": [
// Admins can access everything:
{"action": "accept", "src": ["group:admin"], "dst": ["*:*"]},
// Devs can access dev servers:
{"action": "accept", "src": ["group:devs"], "dst": ["tag:dev-server:*"]},
// Everyone can ping:
{"action": "accept", "src": ["*"], "dst": ["*:icmp"]},
// Deny everything else:
{"action": "deny", "src": ["*"], "dst": ["*:*"]}
],
"tagOwners": {
"tag:dev-server": ["group:admin"]
}
}
# Apply ACL policy:
docker exec headscale headscale policy set --policy-file /etc/headscale/acls.hujson
Part 5: Subnet Routing
Route your home network (192.168.1.0/24) through a Headscale node:
# On the node that's on the subnet you want to route:
tailscale up --login-server https://headscale.yourdomain.com \
--advertise-routes=192.168.1.0/24
# On the Headscale server, approve the route:
docker exec headscale headscale routes list
docker exec headscale headscale routes enable --route 1 # Route ID from list
# Now all Headscale nodes can reach 192.168.1.0/24 via this node
Use cases:
- Access your home NAS (192.168.1.x) from anywhere
- Route office network to remote workers
- Self-hosted services on internal LAN accessible from outside
Part 6: Exit Nodes
Route all internet traffic through a Headscale node:
# On the node to use as exit:
tailscale up --login-server https://headscale.yourdomain.com \
--advertise-exit-node
# Approve on Headscale server:
docker exec headscale headscale routes enable --route 2 # exit route ID
# On client — use this node as internet exit:
tailscale up --exit-node=100.64.0.5 # Exit node's Tailscale IP
Useful for:
- Route traffic through a specific datacenter
- Bypass geo-restrictions
- Privacy when on untrusted WiFi
Part 7: Mobile Clients
iOS and Android use the standard Tailscale app with a custom control server:
iOS:
- Install Tailscale from App Store
- Open → Settings (top right) → Accounts → Login with custom server
- Server URL:
https://headscale.yourdomain.com - Login → opens Safari → register node
Android:
- Install Tailscale
- Tap the three-dot menu → Use a different server
- Enter your Headscale URL
Maintenance
# Update Headscale:
docker compose pull
docker compose up -d
# Backup (SQLite database + config):
cp ./data/db.sqlite ./data/db.sqlite.bak
tar -czf headscale-backup-$(date +%Y%m%d).tar.gz ./config ./data
# Revoke a node:
docker exec headscale headscale nodes delete --identifier 3
# Expire auth keys:
docker exec headscale headscale preauthkeys list --user alice
# Remove expired nodes:
docker exec headscale headscale nodes expire --identifier 5
# Check node status:
docker exec headscale headscale nodes list
Resource Usage
Headscale is extremely lightweight:
Idle RAM: ~30MB (just the Go binary)
Active nodes: +~1MB per node
CPU: Near zero between key exchanges
Storage: ~1MB SQLite for hundreds of nodes
Headscale can comfortably run on the smallest VPS alongside many other services.
See all open source VPN and networking tools at OSSAlt.com/categories/networking.