Knowledgebase › Headscale on a LYLIX VPS — running your own Tailscale control plane

Headscale on a LYLIX VPS — running your own Tailscale control plane

Tailscale's client is excellent but the control plane is hosted on Tailscale's infrastructure. Headscale is an open-source reimplementation of that control plane that you can self-host. Combined with the official Tailscale clients, you get a fully-self-hosted mesh network. This article covers the setup.

Why bother

  • Tailscale's free tier is generous but capped at 100 devices and three users. Headscale has no caps.
  • Privacy: your coordination metadata stays on your server.
  • Air-gap compatibility: you can run Headscale on a network that doesn't reach Tailscale's servers.
  • Compliance: data residency for the coordination layer.

Trade-offs:

  • You operate it. Tailscale's hosted plane never breaks.
  • Some Tailscale features (Funnel, MagicDNS for shared domains) are not fully replicated.
  • Slightly behind Tailscale on protocol features.

Sizing

Headscale is light:

  • 1 GB RAM, 1 CPU, minimum disk — handles hundreds of clients.
  • The control plane processes coordination messages, not traffic — traffic flows directly between peers via WireGuard.

Install

Headscale ships as a single binary. On Debian:

# Latest release from https://github.com/juanfont/headscale/releases
HS_VER=0.23.0
wget https://github.com/juanfont/headscale/releases/download/v${HS_VER}/headscale_${HS_VER}_linux_amd64.deb
dpkg -i headscale_${HS_VER}_linux_amd64.deb

# Or the binary directly if no .deb available
wget -O /usr/local/bin/headscale \
    https://github.com/juanfont/headscale/releases/latest/download/headscale_linux_amd64
chmod +x /usr/local/bin/headscale

Configuration

Default config at /etc/headscale/config.yaml. The minimal viable config:

server_url: https://headscale.example.com
listen_addr: 127.0.0.1:8080
metrics_listen_addr: 127.0.0.1:9090

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

ip_prefixes:
    - 100.64.0.0/10
    - fd7a:115c:a1e0::/48

derp:
    server:
        enabled: false
    urls:
        - https://controlplane.tailscale.com/derpmap/default

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

dns:
    magic_dns: true
    base_domain: example.com
    nameservers:
        global:
            - 1.1.1.1
            - 9.9.9.9

Note the DERP servers: Headscale defaults to using Tailscale's public DERP relays for NAT traversal. You can run your own DERP servers if you prefer; for most home/small-business deployments, public DERP is fine.

nginx reverse proxy

server {
    listen 443 ssl;
    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;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_http_version 1.1;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Start Headscale

systemctl enable --now headscale

Create a user and namespace

Headscale calls user-scopes "users":

headscale users create myuser

Connect a client

Install the official Tailscale client on your device, then:

tailscale up --login-server https://headscale.example.com

The client prints a URL. Open it; you're sent to Headscale's auth page. Run on the Headscale server to authorize:

headscale nodes register --user myuser --key <key-from-url>

The node is now in your tailnet. Repeat for each device.

Pre-auth keys for unattended setup

For deploying clients programmatically:

# Generate a pre-auth key (reusable for 24 hours)
headscale preauthkeys create --user myuser --reusable --expiration 24h

# On the client
tailscale up --login-server https://headscale.example.com --auth-key <key>

ACLs

By default, every node can reach every other node in your tailnet. For multi-tenant or stricter setups, use Tailscale- compatible ACL syntax:

# /etc/headscale/acls.json
{
    "tagOwners": {
        "tag:server": ["myuser"],
        "tag:laptop": ["myuser"]
    },
    "acls": [
        // Laptops can reach servers; servers can't reach laptops
        { "action": "accept", "src": ["tag:laptop"], "dst": ["tag:server:*"] }
    ]
}

Reference in config.yaml:

policy:
    mode: file
    path: /etc/headscale/acls.json

MagicDNS

If magic_dns: true in config, every node gets a DNS name like nodename.username.example.com. Other nodes can ping each other by name.

Inside the tailnet only — DNS isn't published publicly.

Subnet routes

Expose your LYLIX VPS as a gateway to its on-LAN resources (if any) or use as an exit node:

# Advertise routes
tailscale up --login-server https://headscale.example.com \
    --advertise-routes=192.168.1.0/24

# Approve on Headscale
headscale routes enable -r <route-id>

Backups

  • SQLite DB: copy /var/lib/headscale/db.sqlite nightly.
  • Private keys: /var/lib/headscale/{private,noise_private}.key. If you lose these, every node has to re-register.
  • Config and ACL files.

Multi-user / multi-tenant

Each Headscale "user" is an isolated namespace by default. For organisations with multiple users:

headscale users create alice
headscale users create bob

By default, alice's nodes can't see bob's. Use ACLs to allow cross-user routing where needed.

When to stick with Tailscale SaaS

  • You're under 100 devices and 3 users — free tier covers you.
  • You don't want to operate one more service.
  • You need features Headscale hasn't replicated yet (Funnel, app connectors).

For most personal and small-org use, Tailscale's hosted service is fine. Headscale shines when you outgrow free or have specific reasons to keep coordination in-house.

Also Read

« « Back

Powered by WHMCompleteSolution