Headscale: The Complete Self-Hosted Tailscale Guide

Deploy your own zero-trust mesh VPN infrastructure. Learn how to run Headscale, configure ACL policies, integrate OIDC authentication, and manage thousands of nodes without relying on third-party control planes.

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:

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
💡 Use Case Analysis

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:

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.

⚠️ Important Distinction

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.

docker-compose.yml
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
1

Create Configuration Directory

mkdir -p ~/headscale/{config,data}
cd ~/headscale
2

Generate Configuration

docker run --rm -v $(pwd)/config:/etc/headscale \
  headscale/headscale:0.23.0 configdump > config/config.yaml
3

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
4

Start the Server

docker compose up -d

Binary Installation

For bare metal or VM deployment without Docker:

Install on Linux
# 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
Systemd Service
# /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:

Caddyfile
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
🔐 Security Best Practice

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 and Configure
# 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:

  1. Install Tailscale from App Store
  2. Open Settings → Tailscale → Alternative Coordination Server
  3. Enter your Headscale URL: https://headscale.yourdomain.com
  4. Return to app and authenticate
⚠️ iOS Limitation

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.

Basic ACL Policy
// /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
✓ Testing ACLs

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:

Azure AD Configuration
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
Google 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:

  1. Run multiple Headscale instances behind a load balancer
  2. Use PostgreSQL with read replicas
  3. 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
Debug Commands
# 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.