Skip to main content

How to Self-Host Headscale: Open Source Tailscale Control Server 2026

·OSSAlt Team
headscaletailscalevpnwireguardnetworkingself-hostingdocker2026

TL;DR

Headscale (BSD 3-Clause, ~18K GitHub stars, Go) is an open source, self-hosted implementation of the Tailscale coordination server. It lets you run your own Tailscale-compatible VPN mesh with all official Tailscale clients — macOS, Windows, iOS, Android, Linux. Tailscale's free tier is limited to 3 users and 100 devices; the Teams plan is $6/user/month. Headscale is free with unlimited users and devices. You run the control plane; WireGuard handles the actual encrypted tunnels between devices.

Key Takeaways

  • Headscale: BSD 3-Clause, ~18K stars — open source Tailscale control server
  • All official clients: Use the official Tailscale apps on iOS, Android, macOS, Windows, Linux
  • WireGuard mesh: Devices connect peer-to-peer via WireGuard — you don't proxy traffic
  • Unlimited users/devices: No per-seat pricing
  • Exit nodes: Route all internet traffic through a specific device
  • Split tunneling: Only route specific subnets through the VPN

How Tailscale / Headscale Works

Without Tailscale:
  Device A ←→ Internet ←→ Device B (blocked by NAT/firewall)

With Tailscale/Headscale:
  Device A ←→ [WireGuard encrypted tunnel] ←→ Device B
  (direct peer-to-peer after initial coordination)

Control plane (Headscale): Manages identity, key exchange, routing tables
Data plane (WireGuard): Encrypted p2p tunnels between devices

Part 1: Server Setup

Docker Setup

# docker-compose.yml
services:
  headscale:
    image: headscale/headscale:latest
    container_name: headscale
    restart: unless-stopped
    ports:
      - "8080:8080"     # gRPC/HTTP API
      - "9090:9090"     # Metrics
    volumes:
      - ./config:/etc/headscale
      - headscale_data:/var/lib/headscale
    command: headscale serve

volumes:
  headscale_data:
mkdir config

Configuration

# config/config.yaml
server_url: https://headscale.yourdomain.com

listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090

private_key_path: /var/lib/headscale/private.key
noise:
  private_key_path: /var/lib/headscale/noise_private.key

ip_prefixes:
  - fd7a:115c:a1e0::/48    # IPv6
  - 100.64.0.0/10           # IPv4 CGNAT range (Tailscale default)

derp:
  server:
    enabled: false          # Disable embedded DERP — use Tailscale's DERP or run your own
  urls:
    - https://controlplane.tailscale.com/derpmap/default

disable_check_updates: true

database:
  type: sqlite3
  sqlite:
    path: /var/lib/headscale/db.sqlite

log:
  level: info
docker compose up -d

HTTPS with Caddy

headscale.yourdomain.com {
    reverse_proxy localhost:8080
}

Part 2: Create Users

In Headscale, "users" are namespaces (one user per device group or person):

# Create a user:
docker exec headscale headscale users create alice

# List users:
docker exec headscale headscale users list

Part 3: Connect Devices

Linux (CLI)

# Install Tailscale client:
curl -fsSL https://tailscale.com/install.sh | sh

# Connect to your Headscale server instead of Tailscale:
sudo tailscale up \
  --login-server https://headscale.yourdomain.com \
  --accept-routes \
  --accept-dns=false

# This outputs a URL — register the device:
# https://headscale.yourdomain.com/register/MKEY:...

On the Headscale server, register the device:

# Get pending registration requests:
docker exec headscale headscale nodes register --user alice --key MKEY:your-machine-key

# List registered nodes:
docker exec headscale headscale nodes list

macOS / Windows / iOS / Android

  1. Install the official Tailscale app
  2. Not signed in yet → click "Sign in"
  3. The sign-in page appears
  4. Change server / Use a custom serverhttps://headscale.yourdomain.com
  5. A URL appears — register via Headscale CLI:
    docker exec headscale headscale nodes register --user alice --key nodekey:...
    

Using Headscale UI (Optional)

Headscale UI adds a web GUI:

# Add to docker-compose.yml:
services:
  headscale-ui:
    image: ghcr.io/gurucomputing/headscale-ui:latest
    container_name: headscale-ui
    restart: unless-stopped
    ports:
      - "8082:80"
headscale.yourdomain.com {
    handle /web* {
        reverse_proxy localhost:8082
    }
    reverse_proxy localhost:8080
}

Part 4: Key Management

# Generate a pre-auth key (for automated device enrollment):
docker exec headscale headscale preauthkeys create \
  --user alice \
  --reusable \
  --expiration 24h

# Output: nodekey:your-preauth-key

# Use on device (no manual registration required):
sudo tailscale up \
  --login-server https://headscale.yourdomain.com \
  --auth-key nodekey:your-preauth-key

Pre-auth keys are useful for:

  • Automating server enrollment in Ansible/Terraform
  • Docker containers joining the tailnet

Part 5: Exit Nodes

Route all internet traffic through a specific device (like a home server):

On the exit node device:

sudo tailscale up --login-server https://headscale.yourdomain.com --advertise-exit-node

Approve on Headscale:

docker exec headscale headscale routes list
docker exec headscale headscale routes enable -r ROUTE_ID

On client devices:

# Route all traffic through the exit node:
sudo tailscale up --exit-node=100.64.0.1

# Or just specific subnets:
sudo tailscale up --advertise-routes=192.168.1.0/24

Part 6: Subnet Routing (Access Home Network)

Expose your home LAN to all Tailscale devices:

# On your home server:
sudo tailscale up \
  --login-server https://headscale.yourdomain.com \
  --advertise-routes=192.168.1.0/24

# Approve on Headscale:
docker exec headscale headscale routes enable -r ROUTE_ID

# On other devices — access home devices by their LAN IP:
# ssh 192.168.1.100  ← your home NAS, from anywhere

Part 7: DNS with MagicDNS

Tailscale's MagicDNS gives each device a name (alice-laptop.headscale.yourdomain.com):

# config/config.yaml
dns_config:
  magic_dns: true
  base_domain: vpn.yourdomain.com
  nameservers:
    - 1.1.1.1
    - 8.8.8.8
  override_local_dns: false

Devices are accessible via device-name.vpn.yourdomain.com from any Headscale node.


Maintenance

# Update Headscale:
docker compose pull
docker compose up -d

# Node management:
docker exec headscale headscale nodes list
docker exec headscale headscale nodes delete --identifier NODE_ID

# User management:
docker exec headscale headscale users list
docker exec headscale headscale users rename --name alice --new-name alice-work

# Backup (SQLite):
tar -czf headscale-backup-$(date +%Y%m%d).tar.gz \
  $(docker volume inspect headscale_headscale_data --format '{{.Mountpoint}}') \
  ./config/

# Logs:
docker compose logs -f headscale

See all open source networking and VPN tools at OSSAlt.com/categories/networking.

Comments