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.sqlitenightly. - 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
Powered by WHMCompleteSolution