Press ESC to close Press / to search

Headscale: Self-Host Your Own Tailscale VPN Coordination Server on Linux

🎯 Key Takeaways

  • Table of Contents
  • How Headscale Works
  • Prerequisites
  • Installing and Configuring Headscale
  • TLS and Reverse Proxy Setup

📑 Table of Contents

Tailscale makes WireGuard mesh networking effortless, but it requires trusting Tailscale’s coordination server with your network topology and authentication. Headscale is the open-source, self-hosted reimplementation of that coordination server — you run it on your own Linux server, your clients connect to it exactly like they connect to Tailscale, and you retain complete control over your mesh VPN with no data leaving your infrastructure.

Table of Contents

How Headscale Works

Tailscale’s architecture separates the data plane (actual encrypted WireGuard traffic between peers) from the control plane (key exchange, peer discovery, and authentication). Tailscale’s coordination server handles the control plane. When you replace it with Headscale, every client node still runs the standard Tailscale client software — Headscale is a drop-in replacement for the coordination server only. The WireGuard tunnels between your devices are established exactly as with commercial Tailscale, peer-to-peer wherever possible, with DERP relay servers used as fallback when direct connection is blocked.

Your Headscale server needs to be internet-reachable on port 443 (HTTPS). Clients do not route traffic through the Headscale server itself — it only coordinates key exchange and peer discovery.

Prerequisites

  • A Linux VPS or server with a public IP address
  • A domain name pointing to your server (e.g., headscale.example.com)
  • Port 443 (HTTPS) and port 3478/UDP (STUN) open in your firewall
  • Tailscale client installed on devices you want to connect

Installing and Configuring Headscale

Install the Headscale Binary

# Get the latest release version
HEADSCALE_VERSION=$(curl -s https://api.github.com/repos/juanfont/headscale/releases/latest \
  | grep tag_name | cut -d '"' -f 4 | tr -d 'v')

# Download for your architecture
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
wget "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${ARCH}"

# Install
chmod +x headscale_${HEADSCALE_VERSION}_linux_${ARCH}
mv headscale_${HEADSCALE_VERSION}_linux_${ARCH} /usr/local/bin/headscale

# Verify
headscale version

Create System User and Directories

useradd --system --create-home --home-dir /var/lib/headscale \
  --shell /usr/sbin/nologin headscale

mkdir -p /etc/headscale /var/lib/headscale /var/run/headscale
chown -R headscale:headscale /var/lib/headscale /var/run/headscale

Configure Headscale

cat > /etc/headscale/config.yaml << 'EOF'
# Headscale configuration
server_url: https://headscale.example.com  # Your public domain

listen_addr: 127.0.0.1:8080    # Listen on localhost (nginx proxies to this)
metrics_listen_addr: 127.0.0.1:9090

# Private key for the server (auto-generated on first start)
private_key_path: /var/lib/headscale/private.key
noise:
  private_key_path: /var/lib/headscale/noise_private.key

# IP address pool for the mesh network
ip_prefixes:
  - 100.64.0.0/10    # Tailscale's CGNAT range (RFC 6598)

# Database (SQLite is fine for small deployments; PostgreSQL for larger ones)
db_type: sqlite3
db_path: /var/lib/headscale/db.sqlite

# DERP relay servers (use Tailscale's public DERPs or run your own)
derp:
  urls:
    - https://controlplane.tailscale.com/derpmap/default
  auto_update_enabled: true
  update_frequency: 24h

# DNS configuration for the mesh
dns:
  magic_dns: true
  base_domain: mesh.example.com    # .mesh.example.com as domain suffix
  nameservers:
    - 1.1.1.1
    - 8.8.8.8

# Authentication
oidc:
  # Optional: configure OIDC for SSO (Authentik, Keycloak, etc.)
  # Leave blank to use pre-authentication keys

# Logging
log:
  level: info
  format: text
EOF

Create systemd Service

cat > /etc/systemd/system/headscale.service << 'EOF'
[Unit]
Description=Headscale — Self-Hosted Tailscale Coordination Server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=headscale
Group=headscale
ExecStart=/usr/local/bin/headscale serve
Restart=on-failure
RestartSec=5s
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable headscale

TLS and Reverse Proxy Setup

Headscale must be served over HTTPS. The recommended approach is an nginx reverse proxy with a Let’s Encrypt certificate.

Obtain TLS Certificate with Certbot

apt install -y certbot python3-certbot-nginx  # Ubuntu/Debian
# or
dnf install -y certbot python3-certbot-nginx  # RHEL/Rocky

certbot certonly --standalone -d headscale.example.com \
  --email admin@example.com --agree-tos --non-interactive

Nginx Configuration

cat > /etc/nginx/sites-available/headscale << 'EOF'
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name headscale.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name headscale.example.com;

    ssl_certificate     /etc/letsencrypt/live/headscale.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/headscale.example.com/privkey.pem;

    # Required for Tailscale client compatibility
    proxy_read_timeout  900s;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;
    }
}
EOF

ln -s /etc/nginx/sites-available/headscale /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

# Now start Headscale
systemctl start headscale
systemctl status headscale

Creating Users and Namespaces

# Create a user (formerly called "namespace" in older Headscale versions)
headscale users create alice

# List users
headscale users list

# Generate a pre-authentication key for a user (single-use, for one device)
headscale preauthkeys create --user alice

# Generate a reusable pre-auth key (for multiple devices or automation)
headscale preauthkeys create --user alice --reusable --expiration 24h

# List pre-auth keys
headscale preauthkeys list --user alice

Connecting Linux Clients

# Install Tailscale client on the Linux node you want to connect
curl -fsSL https://tailscale.com/install.sh | sh

# Connect to your Headscale server instead of Tailscale's servers
tailscale up --login-server=https://headscale.example.com

# This outputs a URL — open it or use a pre-auth key instead:
tailscale up \
  --login-server=https://headscale.example.com \
  --authkey=

# Verify connection
tailscale status
tailscale ip

Approve the Node on the Server

# On the Headscale server, list nodes waiting for approval
headscale nodes list

# Register a node using the auth key URL (from tailscale up output)
headscale nodes register --user alice --key nodekey:xxxxxxxxxxxxxxxx

# Or if using pre-auth keys, nodes register automatically

Connecting Windows and macOS Clients

# macOS (using the Tailscale.app from Mac App Store or CLI)
# Option 1: CLI
brew install tailscale
tailscale up --login-server=https://headscale.example.com --authkey=

# Option 2: macOS App — hold Alt/Option when clicking the menu bar icon
# → "Use custom coordination server" → enter https://headscale.example.com

# Windows — using tailscale CLI (installed via winget or MSI)
# Run in PowerShell as Administrator:
tailscale up --login-server=https://headscale.example.com --authkey=

Connecting Android and iOS

# Android: Install Tailscale from Play Store
# → Tap the three-dot menu → "Use custom coordination server"
# → Enter: https://headscale.example.com

# iOS: Install Tailscale from App Store
# → Settings → Custom Coordination Server
# → Enter: https://headscale.example.com

# After setting the server, log in — it redirects to your Headscale login flow
# Use a pre-auth key to authenticate without a browser login page

Configuring Exit Nodes

An exit node routes all internet traffic for clients through itself — useful for routing traffic through a trusted server when on untrusted networks.

# On the Linux node you want to use as an exit node:
echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.d/99-headscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' >> /etc/sysctl.d/99-headscale.conf
sysctl --system

# Advertise as an exit node
tailscale up \
  --login-server=https://headscale.example.com \
  --advertise-exit-node \
  --authkey=

# On the Headscale server, approve the exit node route
headscale routes list
headscale routes enable --route 

# On client nodes, select the exit node
tailscale up --exit-node=

Access Control Lists (ACLs)

Headscale supports Tailscale’s ACL format (HuJSON) to control which nodes can talk to which. By default all nodes in the same user can reach each other.

# /etc/headscale/acls.yaml — example policy
acls:
  # Allow all nodes in the 'devs' user to reach all ports on servers
  - action: accept
    src:
      - "devs:*"
    dst:
      - "servers:*:*"

  # Allow SSH from any node to any node
  - action: accept
    src:
      - "*"
    dst:
      - "*:22"

  # Block everything not explicitly allowed
  # (Headscale defaults to deny-all if ACLs are configured)
# Apply the ACL policy
headscale policy set --file /etc/headscale/acls.yaml

# Verify current policy
headscale policy get

Headscale-UI: Web Interface

Headscale’s CLI is powerful but a web interface makes node management easier, especially for multiple users. The community-maintained headscale-ui project provides a React dashboard.

# Run headscale-ui as a Docker container (no official package yet)
docker run -d \
  --name headscale-ui \
  --restart unless-stopped \
  -p 8443:443 \
  -e HS_SERVER=https://headscale.example.com \
  -e HS_API_KEY=$(headscale apikeys create --expiration 9999d) \
  ghcr.io/gurucomputing/headscale-ui:latest

Maintenance and Upgrades

# Backup the database before upgrades
systemctl stop headscale
cp /var/lib/headscale/db.sqlite /var/backups/headscale-$(date +%Y%m%d).sqlite

# Upgrade binary
HEADSCALE_VERSION=
wget "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_amd64"
chmod +x headscale_${HEADSCALE_VERSION}_linux_amd64
mv headscale_${HEADSCALE_VERSION}_linux_amd64 /usr/local/bin/headscale
systemctl start headscale

# Expire old pre-auth keys
headscale preauthkeys expire --user alice --key 

# Remove stale nodes (offline for more than 30 days)
headscale nodes list
headscale nodes delete --node 

Troubleshooting

Client Cannot Connect to Headscale

# Verify Headscale is reachable from the client
curl -v https://headscale.example.com/health

# Check Headscale service logs
journalctl -u headscale -f

# Verify port 443 is open
ss -tlnp | grep nginx
nft list ruleset | grep 443

Nodes Can’t Reach Each Other

# Check that both nodes show the other as a peer
tailscale status

# Confirm routes are active on Headscale server
headscale routes list

# Test connectivity with Tailscale's built-in ping (bypasses OS ping)
tailscale ping 100.64.0.2

# Check firewall on the Headscale server (STUN port must be open)
nft list ruleset | grep 3478

Certificate Issues

# Test TLS from outside
openssl s_client -connect headscale.example.com:443 -servername headscale.example.com

# Renew certificate
certbot renew --nginx
systemctl reload nginx

Conclusion

Headscale gives you everything Tailscale offers — zero-config WireGuard mesh networking, mobile client support, exit nodes, and MagicDNS — with complete control over your coordination server. Your network topology, device keys, and authentication data stay on infrastructure you own. The standard Tailscale client on every platform (Linux, macOS, Windows, iOS, Android) works without modification, which means you benefit from Tailscale’s years of client development while maintaining full operational independence. A single VPS with 1 GB RAM running Headscale can comfortably coordinate a mesh of hundreds of devices.

Was this article helpful?

Advertisement
🏷️ Tags: headscale linux vpn mesh vpn self-hosted tailscale vpn wireguard wireguard vpn zero trust
R

About Ramesh Sundararamaiah

Red Hat Certified Architect

Expert in Linux system administration, DevOps automation, and cloud infrastructure. Specializing in Red Hat Enterprise Linux, CentOS, Ubuntu, Docker, Ansible, and enterprise IT solutions.

🐧 Stay Updated with Linux Tips

Get the latest tutorials, news, and guides delivered to your inbox weekly.

Advertisement

Add Comment