Tailscale is brilliant — it creates a zero-config mesh VPN using WireGuard that just works. But it routes all coordination through Tailscale’s servers. If you want that same magic while keeping full control, Headscale is the answer.

Headscale is an open-source, self-hosted implementation of the Tailscale coordination server. Your devices still use the official Tailscale client, but they talk to your server instead of Tailscale’s cloud. Same great UX, complete sovereignty.

Why Headscale?

  • Full control — your coordination server, your rules
  • No account limits — Tailscale’s free tier caps at 100 devices; Headscale has no limits
  • Privacy — connection metadata never leaves your infrastructure
  • Same clients — uses official Tailscale apps on every platform
  • WireGuard underneath — proven, audited crypto
  • ACLs — fine-grained access control between devices
  • Exit nodes — route internet traffic through any device
  • MagicDNS — automatic DNS for all your devices

How It Works

[[PLhaopnteo]p][H(ecaodosrcdailneatSieornv)er][[HNoAmSe/DSoecrkveerr]]

Headscale handles key exchange and device registration. Actual traffic flows directly between devices via WireGuard (peer-to-peer) — your server isn’t a bottleneck.

Prerequisites

  • A Linux server with a public IP (VPS works great — $5/month)
  • A domain name pointed at that server
  • Docker and Docker Compose
  • SSL certificate (we’ll use Caddy for auto-HTTPS)

Note: Headscale works best on a VPS with a stable public IP. You can run it at home, but you’ll need port forwarding and dynamic DNS.

Step 1: Project Setup

mkdir -p ~/headscale/config && cd ~/headscale

Step 2: Generate Configuration

# Download the example config
wget -O config/config.yaml \
  https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml

Edit config/config.yaml with the essential settings:

# The URL clients will use to reach your Headscale server
server_url: https://hs.yourdomain.com

# Listen addresses
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090

# gRPC for CLI
grpc_listen_addr: 0.0.0.0:50443
grpc_allow_insecure: false

# Private key storage
private_key_path: /var/lib/headscale/private.key
noise:
  private_key_path: /var/lib/headscale/noise_private.key

# IP ranges for your mesh network
prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48

# DERP (relay servers for when direct connections fail)
derp:
  server:
    enabled: true
    region_id: 999
    region_code: "self"
    region_name: "Self-Hosted"
    stun_listen_addr: "0.0.0.0:3478"
  urls: []
  auto_update_enabled: false

# Database
database:
  type: sqlite
  sqlite:
    path: /var/lib/headscale/db.sqlite

# DNS configuration
dns:
  magic_dns: true
  base_domain: mesh.local
  nameservers:
    global:
      - 1.1.1.1
      - 9.9.9.9

Key changes from the example:

  • Set server_url to your actual domain
  • Enable the built-in DERP relay
  • Configure MagicDNS with a base domain
  • Use SQLite (simple, no extra services needed)

Step 3: Docker Compose

Create docker-compose.yml:

services:
  headscale:
    image: headscale/headscale:latest
    container_name: headscale
    volumes:
      - ./config:/etc/headscale
      - headscale-data:/var/lib/headscale
    ports:
      - "8080:8080"
      - "9090:9090"
      - "3478:3478/udp"
    command: serve
    restart: unless-stopped

  caddy:
    image: caddy:latest
    container_name: headscale-caddy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy-data:/data
    restart: unless-stopped

volumes:
  headscale-data:
  caddy-data:

Create Caddyfile:

h}s.yoruervdeormsaei_np.rcooxmy{headscale:8080

Step 4: Start Headscale

docker compose up -d

Verify it’s running:

docker exec headscale headscale version

Step 5: Create a User

Headscale organizes devices under “users” (similar to Tailscale’s tailnets):

# Create a user for yourself
docker exec headscale headscale users create bird

# List users
docker exec headscale headscale users list

Step 6: Register Devices

Linux

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

# Connect to YOUR Headscale server
tailscale up --login-server https://hs.yourdomain.com

This prints a registration URL. Either:

Option A: Register via CLI (on the server)

docker exec headscale headscale nodes register \
  --user bird \
  --key mkey:abc123...

Option B: Use a pre-auth key

# Generate a key on the server
docker exec headscale headscale preauthkeys create \
  --user bird \
  --reusable \
  --expiration 24h

# Use it on the client
tailscale up --login-server https://hs.yourdomain.com --authkey <key>

Pre-auth keys are great for automating device registration (Docker containers, cloud VMs, etc.).

macOS

  1. Install Tailscale from the App Store
  2. Open Terminal:
    tailscale up --login-server https://hs.yourdomain.com
    
  3. Register the node on your server

Windows

  1. Install Tailscale from tailscale.com/download
  2. Open Command Prompt as admin:
    tailscaleup-login-serverhttps://hs.yourdomain.com

iOS / Android

The official Tailscale apps support custom coordination servers:

  1. Install the Tailscale app
  2. Before signing in, tap the three dots → “Use custom coordination server”
  3. Enter https://hs.yourdomain.com
  4. Complete registration

Step 7: Verify Your Mesh

From any connected device:

# List all devices in your network
tailscale status

# Ping another device
tailscale ping home-server

# Check direct vs relayed connection
tailscale netcheck

Step 8: Access Control Lists (ACLs)

Control which devices can talk to each other. Create config/acl.yaml:

# Allow all devices to reach the home server
acls:
  - action: accept
    src: ["*"]
    dst: ["home-server:*"]

  # Allow laptop to reach everything
  - action: accept
    src: ["bird-laptop"]
    dst: ["*:*"]

  # Allow phone only to access specific ports
  - action: accept
    src: ["bird-phone"]
    dst: ["home-server:80,443,8080"]

Apply ACLs:

docker exec headscale headscale policy set -f /etc/headscale/acl.yaml

Step 9: Exit Nodes

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

On the exit node device:

# Enable IP forwarding
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

# Advertise as exit node
tailscale up --login-server https://hs.yourdomain.com --advertise-exit-node

Approve it on the server:

docker exec headscale headscale routes enable -r <route-id>

On your phone/laptop:

tailscale up --exit-node=home-server

Now all traffic routes through your home server — instant VPN from anywhere.

Step 10: Subnet Routes

Expose your entire home network to mesh devices:

# On a device on your home LAN
tailscale up --login-server https://hs.yourdomain.com \
  --advertise-routes=192.168.1.0/24

Approve on the server:

docker exec headscale headscale routes list
docker exec headscale headscale routes enable -r <route-id>

Now any mesh device can reach 192.168.1.x addresses directly — access printers, NAS, cameras, anything on your LAN from anywhere.

Headscale UI (Optional)

For a web dashboard, add Headscale-UI:

  headscale-ui:
    image: ghcr.io/gurucomputing/headscale-ui:latest
    container_name: headscale-ui
    ports:
      - "8443:443"
    restart: unless-stopped

Add to Caddyfile:

u}i.hsr}.eyvoeurt}rsrdeao_nmpstarplioosnxr_.yticnohhsmeteatc{dpusrc{ea_lsek-iupi_:v4e4r3if{y

Troubleshooting

Device can’t connect to Headscale

  • Verify DNS: dig hs.yourdomain.com should return your server’s IP
  • Check HTTPS: curl https://hs.yourdomain.com should respond
  • Check firewall: ports 80, 443, 3478/udp must be open
  • View logs: docker compose logs headscale

Devices connected but can’t reach each other

  • Check tailscale status — devices should show as online
  • Run tailscale ping <device> — shows if connection is direct or relayed
  • Enable the DERP relay if direct connections fail (NAT issues)
  • Verify ACLs aren’t blocking traffic

Slow connections

  • Check if traffic is being relayed: tailscale netcheck
  • Open UDP port 41641 on both ends for direct WireGuard connections
  • The DERP relay is a fallback — direct connections are always faster

MagicDNS not resolving

  • Verify magic_dns: true in config
  • Restart Tailscale on the client: sudo tailscale down && sudo tailscale up
  • Check /etc/resolv.conf — Tailscale should have added its DNS

Resource Usage

ComponentRAMCPUNotes
Headscale~30MBMinimalCoordination only
Caddy~20MBMinimalTLS termination
DERP relay~10MBLowOnly during relayed connections
Total~60MBMinimal

Headscale vs Alternatives

FeatureHeadscaleTailscaleWireGuard (manual)ZeroTier
Self-hosted controlPartial
Zero config
NAT traversal
Device limitUnlimited100 (free)Unlimited25 (free)
MagicDNS
ACLsManual iptables
Mobile apps✅ (Tailscale)Third-party
Exit nodesManual

Conclusion

Headscale gives you everything great about Tailscale — the seamless mesh networking, NAT traversal, MagicDNS, exit nodes — without depending on anyone else’s infrastructure. Your keys, your coordination, your network.

It’s the ultimate self-hosting flex: a VPN that connects all your devices automatically, works behind any NAT, and runs on a $5 VPS with 60MB of RAM. Set it up once and you’ll wonder how you ever managed without it.