Install Headscale on VPS — Self-Hosted Tailscale Server

Headscale self-hosted Tailscale VPN mesh network on VPS

Headscale is an open-source, self-hosted implementation of the Tailscale control server. Instead of relying on Tailscale's cloud, you run the control plane on your own VPS — giving you unlimited nodes, full privacy, and complete control over your mesh VPN. It's perfect for developers, DevOps teams, and anyone who wants enterprise-grade zero-trust networking at minimal cost.

Headscale vs Tailscale Cloud

FeatureTailscale CloudHeadscale (Self-hosted)
Control serverTailscale Inc.Your own VPS
PricingFree tier / $6+/monthVPS cost only
Node limit3 (free) / unlimited (paid)Unlimited
PrivacyMetadata through TailscaleYou control everything
MagicDNS / ACLFull supportSupported (some features still evolving)
Data plane (WireGuard)Peer-to-peer directSame — peer-to-peer direct
Important: Headscale only manages the control plane (authentication and IP assignment). Actual traffic travels peer-to-peer via WireGuard — it does NOT pass through your VPS (unless DERP relay is needed when direct connection fails).

Requirements

Step 1: Install Headscale

Download the latest binary from GitHub releases:

# Check latest version at https://github.com/juanfont/headscale/releases
HEADSCALE_VERSION="0.25.1"
ARCH="amd64"

wget "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${ARCH}.deb" \
  -O /tmp/headscale.deb

sudo dpkg -i /tmp/headscale.deb
Tip: Check your architecture with dpkg --print-architecture — use arm64 for ARM-based VPS.

Step 2: Configure config.yaml

Create directories and edit the configuration file:

sudo mkdir -p /etc/headscale /var/lib/headscale /var/run/headscale
sudo cp /etc/headscale/config.yaml.example /etc/headscale/config.yaml 2>/dev/null || true

sudo nano /etc/headscale/config.yaml

Key settings to update in config.yaml:

server_url: https://hs.example.com:443
listen_addr: 0.0.0.0:8080

tls_letsencrypt_hostname: hs.example.com   # Let Headscale manage TLS
tls_letsencrypt_challenge_type: HTTP-01

ip_prefixes:
  - 100.64.0.0/10   # Tailscale CGNAT range

dns_config:
  magic_dns: true
  base_domain: mesh.internal
  nameservers:
    - 1.1.1.1
    - 8.8.8.8

db_type: sqlite3
db_path: /var/lib/headscale/db.sqlite

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

Step 3: Open Ports and Start the Service

# Open required ports
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 41641/udp   # WireGuard / DERP relay

# Enable and start service
sudo systemctl enable --now headscale
sudo systemctl status headscale
Tip: If Headscale fails to start, check logs with sudo journalctl -u headscale -f

Step 4: Create a User (Namespace)

Headscale organizes nodes under "users" (formerly called namespaces):

# Create a user for your network
sudo headscale users create mynetwork

# List all users
sudo headscale users list

Step 5: Connect Tailscale Clients to Headscale

On any device you want to join (Linux, macOS, Windows, iOS, Android):

# Linux — install Tailscale client first
curl -fsSL https://tailscale.com/install.sh | sh

# Connect pointing to your Headscale server
sudo tailscale up \
  --login-server https://hs.example.com \
  --authkey=YOUR_AUTH_KEY

Generate auth keys on the server:

# Create a single-use pre-auth key
sudo headscale preauthkeys create --user mynetwork --expiration 24h

# Create a reusable key (for multiple devices)
sudo headscale preauthkeys create --user mynetwork --reusable --expiration 72h

# List all auth keys
sudo headscale preauthkeys list --user mynetwork

Step 6: Manage Nodes

# List all connected nodes
sudo headscale nodes list

# Register a pending node
sudo headscale nodes register --user mynetwork --key MACHINE_KEY

# Rename a node
sudo headscale nodes rename --identifier 1 my-laptop

# Delete a node
sudo headscale nodes delete --identifier 2

Step 7: Configure ACLs (Access Control)

Create /etc/headscale/acls.yaml to control network access:

acls:
  - action: accept
    src:
      - mynetwork
    dst:
      - mynetwork:*

Reload ACLs:

sudo headscale acls -o yaml get
# After editing, reload
sudo systemctl reload headscale
Tip: Use tag-based ACLs to segment dev/staging/prod environments — devices tagged tag:prod can only be accessed on port 443 from tagged tag:dev nodes.

Step 8: Nginx Reverse Proxy (Optional)

If you prefer to run Headscale behind Nginx instead of letting it handle TLS directly:

sudo nano /etc/nginx/sites-available/headscale
server {
    listen 80;
    server_name hs.example.com;
    return 301 https://$host$request_uri;
}

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

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

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
sudo ln -s /etc/nginx/sites-available/headscale /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Verify Headscale Is Working Correctly

# Check service status
sudo systemctl status headscale

# View logs in real-time
sudo journalctl -u headscale -f --since "5 min ago"

# Test API is responding
curl -s https://hs.example.com/api/v1/node \
  -H "Authorization: Bearer YOUR_API_KEY" | python3 -m json.tool

# List all nodes
sudo headscale nodes list

# Test ping between nodes
ping 100.64.0.2   # IP of a node in your mesh network
Tip: If nodes can't ping each other, verify that port 41641/UDP is open on your VPS firewall — this is the WireGuard DERP relay port used when peer-to-peer connections fail.

Troubleshooting Common Issues

Error: "failed to fetch noise key from control"

This error means the Tailscale client cannot connect to your Headscale server. It usually indicates a wrong URL, TLS certificate issue, or firewall blocking port 443.

# Test that the server is reachable and returns a valid key
curl -v https://hs.example.com/key
# Should return JSON with a public key

# Check Headscale logs
sudo journalctl -u headscale -n 50 --no-pager

Error: "tls: failed to verify certificate"

The TLS certificate on the Headscale server is invalid — often caused by a failed Let's Encrypt renewal or the domain not yet pointing to the VPS IP.

# Check certificate validity
openssl s_client -connect hs.example.com:443 -servername hs.example.com \
  &1 | grep -E "subject|expire|Verify"

# Check built-in Headscale TLS cache
sudo ls -la /var/lib/headscale/cache/

Error: "machine is not registered"

The connecting node hasn't been approved yet. You need to manually register it or use a pre-auth key.

# List nodes awaiting approval
sudo headscale nodes list

# Register the pending node
sudo headscale nodes register \
  --user mynetwork \
  --key mkey:MACHINE_KEY_HERE

Error: "database is locked"

SQLite database is locked by another process, or the disk is full.

# Check disk space
df -h /var/lib/headscale

# Check what process is using the db
sudo fuser /var/lib/headscale/db.sqlite

# Restart service
sudo systemctl restart headscale

Nodes connect but can't ping each other

Usually a firewall or routing issue on the destination node. Check iptables rules and IP forwarding.

# Check IP forwarding on VPS
sysctl net.ipv4.ip_forward

# Enable IP forwarding if not set
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

# Check route table on the problematic node
ip route show table 52

Frequently Asked Questions

How many nodes can Headscale support?

Theoretically unlimited since it's open-source running on your own hardware. In practice, SQLite starts to slow down beyond a few hundred nodes — consider migrating to PostgreSQL at that scale by changing db_type: postgres in the config file.

If my VPS goes down, will connected nodes still communicate?

Yes. WireGuard runs peer-to-peer directly — the control plane (Headscale) is only needed for authentication and IP assignment during onboarding. Once nodes are connected, communication continues even if the server goes offline temporarily. New nodes won't be able to join until the server is back up.

Does Headscale work with iOS and Android?

Yes. Install the official Tailscale app on iOS or Android, then choose "Use custom login server" and enter your Headscale URL. It works with every platform that the official Tailscale client supports, since Headscale implements the full Tailscale protocol.