What is Headscale?
Headscale is an open-source, self-hosted implementation of the Tailscale control server. It provides the same networking capabilities as Tailscale's managed service—secure mesh networking, NAT traversal, and zero-configuration VPN—but you retain complete control over your infrastructure and data.
Built by Juan Font and maintained by a growing community, Headscale implements the Tailscale client protocol, allowing you to use official Tailscale clients while managing your own coordination server. This means you get:
- WireGuard-powered mesh networking with automatic NAT traversal
- MagicDNS for automatic service discovery
- ACL-based access control with fine-grained policies
- OIDC integration for enterprise authentication
- Full data sovereignty—your network metadata never leaves your infrastructure
Why Self-Host Your Control Plane?
Tailscale (Managed)
- Zero maintenance overhead
- Professional support included
- Global DERP infrastructure
- Automatic updates & patches
- Per-user pricing model
- Data processed by Tailscale Inc.
Headscale (Self-Hosted)
- Complete data sovereignty
- No per-user licensing costs
- Custom DERP relay deployment
- Audit logging & compliance
- Integration with internal IAM
- Network isolation requirements
Choose Tailscale (managed) when you need immediate deployment, don't have infrastructure expertise, or require their global relay network for performance.
Choose Headscale when you have compliance requirements (GDPR, HIPAA, SOC 2), need unlimited users without per-seat costs, or require air-gapped network environments.
Architecture & How It Works
Headscale operates as a coordination server in a classic "hub-and-spoke" model that's actually a mesh:
The Control Plane
The Headscale server maintains the "network map"—a complete graph of all authorized nodes, their public keys, and their current network addresses. Clients connect to Headscale over HTTPS (or WebSocket for long-polling) to:
- Register new nodes and obtain IP assignments
- Receive updated network maps when topology changes
- Exchange public keys for WireGuard tunnel establishment
- Query MagicDNS records
The Data Plane
Once nodes learn about each other from the control plane, they establish direct WireGuard tunnels using UDP hole punching (STUN). If direct connection fails, traffic flows through DERP (Designated Encrypted Relay for Packets) servers—either Tailscale's public relays or your own.
Headscale only replaces the control plane. The data plane (encrypted WireGuard traffic) flows directly between peers or through DERP relays. Headscale never sees your actual network traffic—only metadata like which nodes exist and their public keys.
Installation & Deployment
Docker Compose Setup (Recommended)
This is the fastest path to production. We'll use SQLite for simplicity—PostgreSQL is recommended for larger deployments.
version: '3.8'
services:
headscale:
image: headscale/headscale:0.23.0
container_name: headscale
restart: unless-stopped
ports:
- "8080:8080" # HTTP API
- "9090:9090" # Metrics
volumes:
- ./config:/etc/headscale
- ./data:/var/lib/headscale
command: serve
# Optional: Custom DERP server for better performance
derper:
image: tailscale/derp:latest
container_name: derper
restart: unless-stopped
ports:
- "3478:3478/udp" # STUN
- "80:80" # HTTP (redirects to HTTPS)
- "443:443" # HTTPS DERP
environment:
- DERP_DOMAIN=derp.yourdomain.com
- DERP_VERIFY_CLIENTS=true
Create Configuration Directory
mkdir -p ~/headscale/{config,data}
cd ~/headscale
Generate Configuration
docker run --rm -v $(pwd)/config:/etc/headscale \
headscale/headscale:0.23.0 configdump > config/config.yaml
Edit Configuration
Modify the generated config with your domain and preferences:
# config/config.yaml
server_url: https://headscale.yourdomain.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090
# Use your own DNS or specify upstreams
dns:
magic_dns: true
base_domain: tailnet.yourdomain.com
nameservers:
global:
- 1.1.1.1
- 8.8.8.8
# IP allocation
ip_prefixes:
- fd7a:115c:a1e0::/48 # IPv6 ULA
- 100.64.0.0/10 # IPv4 CGNAT
# Database (SQLite for small setups, PostgreSQL for production)
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
# Disable default DERP, we'll use our own
derp:
server:
enabled: false
urls:
- https://controlplane.tailscale.com/derpmap/default
paths: []
auto_update_enabled: true
update_frequency: 24h
Start the Server
docker compose up -d
Binary Installation
For bare metal or VM deployment without Docker:
# Download latest release
VERSION=$(curl -s https://api.github.com/repos/juanfont/headscale/releases/latest | grep tag_name | cut -d '"' -f 4)
wget https://github.com/juanfont/headscale/releases/download/${VERSION}/headscale_${VERSION#v}_linux_amd64.deb
# Install
sudo dpkg -i headscale_*.deb
# Create directories
sudo mkdir -p /etc/headscale /var/lib/headscale
# Generate and edit config
sudo headscale configdump > /etc/headscale/config.yaml
sudo nano /etc/headscale/config.yaml
# /etc/systemd/system/headscale.service
[Unit]
Description=headscale controller
After=network.target
[Service]
Type=simple
User=headscale
Group=headscale
ExecStart=/usr/bin/headscale serve
Restart=always
RestartSec=5
WorkingDirectory=/var/lib/headscale
[Install]
WantedBy=multi-user.target
Configuration
Headscale's configuration spans multiple areas. Here are the critical sections:
TLS / SSL
For production, use a reverse proxy (Caddy, Nginx, or Traefik) to handle TLS:
headscale.yourdomain.com {
reverse_proxy localhost:8080
}
OAuth/OIDC Setup
Configure in config.yaml for Google, GitHub, Azure AD, etc.:
oidc:
issuer: "https://accounts.google.com"
client_id: "your-client-id.apps.googleusercontent.com"
client_secret: "your-client-secret"
# Optional: restrict to specific domains
allowed_domains:
- yourdomain.com
# Optional: restrict to specific users
allowed_users:
- [email protected]
# Strip domain from username ([email protected] -> user)
strip_email_domain: true
Enable OIDC before allowing user registrations. Without it, anyone with network access can register nodes. Use --verify-clients on your DERP server to ensure only authorized clients can use your relays.
Client Setup
Headscale is compatible with official Tailscale clients—you just need to point them to your server.
Linux
# Install Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
# Login to your Headscale instance
sudo tailscale up --login-server https://headscale.yourdomain.com
# Generate auth key (on server) for automated setup
headscale apikey create --reusable
# Or use pre-authenticated key
sudo tailscale up --login-server https://headscale.yourdomain.com \
--authkey tskey-auth-xxx
macOS
# Install via Homebrew
brew install tailscale
# Start service
sudo brew services start tailscale
# Configure for Headscale
sudo tailscale up --login-server https://headscale.yourdomain.com
# Or use the GUI app with custom control URL:
For the macOS GUI app: Hold Option and click the Tailscale menu → Preferences → Run custom coordination server.
Windows
# Install via winget
winget install Tailscale.Tailscale
# From Administrator PowerShell, set custom server
& "C:\Program Files\Tailscale\tailscale.exe" up `
--login-server "https://headscale.yourdomain.com"
iOS & Android
Mobile clients require extra configuration. On iOS:
- Install Tailscale from App Store
- Open Settings → Tailscale → Alternative Coordination Server
- Enter your Headscale URL:
https://headscale.yourdomain.com - Return to app and authenticate
iOS requires the server to have valid HTTPS with a trusted certificate (not self-signed). Use Let's Encrypt or similar. The custom server URL must be set before first launch—changing it later requires app reinstallation.
ACL Policies & Security
Access Control Lists (ACLs) define who can talk to whom. Headscale uses Tailscale's HuJSON format—JSON with comments.
// /etc/headscale/acls/acl.hujson
{
// Groups of users
"groups": {
"group:admin": ["[email protected]", "[email protected]"],
"group:dev": ["[email protected]", "[email protected]"],
"group:ops": ["[email protected]"],
},
// Tag-based access (applied to devices)
"tagOwners": {
"tag:server": ["group:ops"],
"tag:database": ["group:ops"],
"tag:ci": ["group:dev"],
},
// Access rules (higher index = lower priority)
"acls": [
// Admins can access everything
{ "action": "accept", "src": ["group:admin"], "dst": ["*:*"] },
// Developers can access CI/build servers
{ "action": "accept", "src": ["group:dev"], "dst": ["tag:ci:22,443,8080"] },
// Developers can access dev databases
{ "action": "accept", "src": ["group:dev"], "dst": ["tag:database:5432"] },
// Ops can access servers (SSH, HTTP, HTTPS)
{ "action": "accept", "src": ["group:ops"], "dst": ["tag:server:22,80,443"] },
// Everyone can access DNS (UDP 53)
{ "action": "accept", "src": ["*"], "dst": ["*:53/udp"] },
// Default deny (implicit)
],
// SSH access controls
"ssh": [
{ "action": "check", "src": ["group:ops"], "dst": ["tag:server"], "users": ["autogroup:nonroot"] },
],
}
Key ACL Concepts
| Concept | Description | Example |
|---|---|---|
src |
Who is initiating the connection | group:dev, [email protected], * |
dst |
Destination with optional port(s) | tag:server:22,443, *:* |
tag |
Applied to devices, not users | tag:server—must be owned to apply |
autogroup |
Built-in groups | autogroup:internet, autogroup:self |
Use tailscale ping and tailscale status to verify connectivity. The tailscale debug command shows which ACL rule allowed or denied a connection.
OIDC Integration
OIDC enables single sign-on and centralized user management. Here's a complete Azure AD example:
oidc:
issuer: "https://login.microsoftonline.com/your-tenant-id/v2.0"
client_id: "your-app-client-id"
client_secret: "your-app-client-secret"
allowed_domains:
- yourdomain.com
# Map Azure AD groups to Headscale groups
scope: ["openid", "profile", "email", "groups"]
# Custom claims mapping
user_profile:
name: "name"
email: "preferred_username"
groups: "groups"
strip_email_domain: false
Provider-Specific Notes
| Provider | Issuer URL | Notes |
|---|---|---|
https://accounts.google.com |
Use Google Workspace for group claims | |
| GitHub | https://github.com |
No groups claim; use teams API separately |
| Okta | https://your-org.okta.com |
Configure group claims in Okta admin |
| Keycloak | https://keycloak.domain/realms/myrealm |
Add group mapper in client scope |
Production Considerations
Database
SQLite works for hundreds of nodes. For thousands, use PostgreSQL:
database:
type: postgres
postgres:
host: postgres.internal
port: 5432
name: headscale
user: headscale
pass: "${DB_PASSWORD}" # Use environment variable
ssl: true
Backups
# Backup SQLite
cp /var/lib/headscale/db.sqlite /backup/headscale-$(date +%Y%m%d).sqlite
# Backup PostgreSQL
pg_dump -h postgres.internal -U headscale headscale > backup.sql
Monitoring
Headscale exposes Prometheus metrics on port 9090. Key metrics:
# Nodes
headscale_node_count
headscale_node_connected
# API
headscale_api_request_duration_seconds
headscale_api_requests_total
# Database
headscale_db_query_duration_seconds
High Availability
Headscale itself is stateless except for the database. For HA:
- Run multiple Headscale instances behind a load balancer
- Use PostgreSQL with read replicas
- Enable Redis for session caching (if using OIDC)
Troubleshooting
| Symptom | Cause | Solution |
|---|---|---|
| "failed to connect to any derp server" | Firewall blocking UDP | Open 3478/UDP for STUN |
| "invalid node key" | Node reinstalled, key mismatch | headscale node delete -i NODE_ID |
| Can't reach nodes via MagicDNS | DNS not configured | Set system DNS to 100.100.100.100 |
| OIDC login fails | Redirect URI mismatch | Add https://headscale.domain/oidc/callback |
| "acls are too restrictive" | Default deny in effect | Add explicit allow rules |
# View node status
headscale node list
headscale node info NODE_ID
# Check routing
tailscale status
tailscale ping NODE_NAME
# Force netmap refresh
tailscale up --force-reauth
# View server logs
docker logs -f headscale
# ACL validation
headscale acl validate -f /etc/headscale/acls/acl.hujson
Conclusion
Headscale brings the power of Tailscale's mesh networking to your own infrastructure. With complete control over your control plane, you can meet strict compliance requirements, integrate with existing identity providers, and scale without per-user costs.
The trade-off is operational responsibility—you're now running a critical piece of infrastructure. Use the patterns in this guide: PostgreSQL for scale, OIDC for authentication, comprehensive ACLs for security, and proper monitoring for observability.
With these foundations, Headscale becomes a robust, production-ready zero-trust networking platform that rivals—and sometimes exceeds—its managed counterpart.