Skip to main content

How to Self-Host Headscale: Tailscale Alternative for Private VPN Mesh 2026

·OSSAlt Team
headscaletailscalewireguardvpnnetworkingself-hostingdocker2026

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

FeatureHeadscaleTailscale FreeTailscale BusinessZeroTier
CostVPS onlyFree (limits)$6/user/monthFree (25 devices)
Max devicesUnlimited100 devicesUnlimited25 (free)
UsersUnlimited3 usersPer planUnlimited
ControlFull (self-hosted)Tailscale's cloudTailscale's cloudZeroTier cloud
iOS/Android appTailscale appTailscale appTailscale appZeroTier app
Exit nodes
Subnet routing
MagicDNSVia DNS configVia DNS
Open sourceClient onlyClient 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:

  1. Install Tailscale from App Store
  2. Open → Settings (top right) → Accounts → Login with custom server
  3. Server URL: https://headscale.yourdomain.com
  4. Login → opens Safari → register node

Android:

  1. Install Tailscale
  2. Tap the three-dot menu → Use a different server
  3. 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.

Comments