I've been using Tailscale in my home lab for a couple of years. It's exceptional software β WireGuard under the hood, dead-simple client setup, works through CGNAT and double-NAT without any config. The problem is that the coordination server (the thing that handles key exchange, device registration, and ACLs) is Tailscale's cloud. For most people that's fine. For me, it started feeling like a dependency I didn't need.
Headscale is an open-source, self-hosted implementation of the Tailscale control server. You run it yourself, your clients connect to it, and the actual WireGuard data plane stays peer-to-peer β Headscale never touches your traffic. If you're already running Proxmox, adding Headscale as an LXC container takes about 30 minutes and gives you a mesh VPN you control end-to-end.
This post covers the full setup: Proxmox LXC, Headscale installation, DNS + HTTPS with a real certificate, connecting clients, and a few gotchas that tripped me up.
Why Bother? Tailscale's Free Tier Is Generous
Fair question. Tailscale's free tier allows up to 3 users and 100 devices. For a solo home lab that's plenty. But there are a few reasons to go the self-hosted route:
Data sovereignty. Device names, IP assignments, and ACL rules all live in Tailscale's cloud when you use the managed service. With Headscale, that metadata stays on your hardware.
No account dependency. Tailscale requires a Google, Microsoft, GitHub, or Apple SSO account for login. Headscale uses pre-shared keys and its own user model β no OAuth dependency.
Unlimited devices across multiple users. If you're managing devices for family members or sharing VPN access with a few trusted people, Headscale has no caps.
It's a great learning exercise. Running a coordination server forces you to understand WireGuard key exchange, DERP relay servers, and how mesh VPNs actually work.
Hardware Baseline
You need a machine running Proxmox that's on 24/7. The Headscale binary itself is tiny β it'll run comfortably on a Beelink EQ14 N150 or even a Raspberry Pi 5. My setup runs on a Beelink EQ12 Pro with a Samsung 990 Pro 2TB NVMe as the primary drive. The LXC container uses about 50MB of RAM at idle β this is not a resource-intensive workload.
You'll also want a UPS if you don't already have one. Headscale going down means all your remote clients lose connectivity. A CyberPower CP600LCD is enough for a mini PC and a switch. Speaking of switches β if your home lab has grown past a few devices, a TP-Link TL-SG108E 8-port managed switch gives you VLAN support without spending serious money.
One more requirement: a public domain name. Headscale needs to be reachable by your clients, and the Tailscale client validates the coordination server's TLS certificate. You need a real cert β Let's Encrypt works fine. I use Cloudflare for DNS with a wildcard cert.
Creating the LXC Container
In Proxmox, create a new LXC container. I use Ubuntu 22.04 LTS as the base template.
# On Proxmox host, pull the template if you don't have it
pveam update
pveam download local ubuntu-22.04-standard_22.04-1_amd64.tar.zst
Container specs that work well:
Via the Proxmox UI: Create CT β select template β set resources β under Options, enable "Nesting" β start the container.
Or via CLI:
pct create 200 local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst \ --hostname headscale \ --cores 2 \ --memory 512 \ --swap 512 \ --rootfs local-lvm:8 \ --net0 name=eth0,bridge=vmbr0,ip=dhcp \ --features nesting=1 \ --unprivileged 1
pct start 200 pct exec 200 -- bash -c "apt-get update && apt-get upgrade -y"
Assign a static IP either via your DHCP server (preferred β reserve by MAC) or set it statically inside the container.
Installing Headscale
Headscale ships as a single binary. Grab the latest release from GitHub:
# Inside the LXC container export HS_VERSION=$(curl -s https://api.github.com/repos/juanfont/headscale/releases/latest \ | grep tag_name | cut -d '"' -f4 | sed 's/v//')
wget -q "https://github.com/juanfont/headscale/releases/download/v${HS_VERSION}/headscale_${HS_VERSION}_linux_amd64.deb" dpkg -i "headscale_${HS_VERSION}_linux_amd64.deb"
The package installs a systemd unit and creates /etc/headscale/config.yaml. Edit that file β this is where most of the setup lives.
nano /etc/headscale/config.yaml
Key fields to set:
# The public FQDN your clients will connect to
server_url: https://headscale.yourdomain.comAddress and port Headscale listens on
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090TLS β either inline certs or let Headscale handle ACME
tls_letsencrypt_hostname: headscale.yourdomain.com
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_challenge_type: TLS-ALPN-01If using Let's Encrypt, Headscale needs to bind 443 directly
listen_addr: 0.0.0.0:443DERP β use Tailscale's public DERP servers or run your own
derp:
urls:
- https://controlplane.tailscale.com/derpmap/default
auto_update_enabled: trueIP range for your Tailnet
ip_prefixes:
- fd7a:115c:a1e0::/48
- 100.64.0.0/10DNS config pushed to clients
dns:
magic_dns: true
base_domain: home.yourdomain.com
nameservers:
global:
- 1.1.1.1
- 8.8.8.8
On the TLS setup: The easiest path is letting Headscale handle its own Let's Encrypt cert via TLS-ALPN-01 challenge. This requires Headscale to bind port 443 directly and have that port reachable from the internet. If you're behind CGNAT or prefer not to open ports, use Cloudflare DNS challenge with a separate ACME client (acme.sh or Certbot) and point Headscale at the cert files:
tls_cert_path: /etc/headscale/certs/cert.pem
tls_key_path: /etc/headscale/certs/key.pem
listen_addr: 0.0.0.0:443
Start and enable Headscale:
systemctl enable --now headscale
journalctl -u headscale -f # watch logs
Creating Users and Pre-Auth Keys
Headscale's user model maps to Tailscale's concept of users. Create one per person (or logical group):
headscale users create homelab
headscale users list
To connect a device, generate a pre-auth key:
# One-time use key, expires in 24 hours
headscale preauthkeys create --user homelab --expiration 24hReusable key (useful for scripting device enrollment)
headscale preauthkeys create --user homelab --reusable --expiration 720h
You'll get a key string like abc123.... Use it on the client:
# On any Linux/macOS/Windows machine with Tailscale installed
tailscale up \
--login-server https://headscale.yourdomain.com \
--authkey <YOUR_PREAUTH_KEY> \
--hostname my-macbook
For iOS and Android, go to Settings β Account β Sign in with another server β enter your Headscale URL, then use the pre-auth key flow.
After connecting, verify on the Headscale side:
headscale nodes list
ACLs and Routes
This is where Headscale really earns its keep. You can define fine-grained ACLs in HuJSON:
nano /etc/headscale/acls.yaml
{
"acls": [
{
"action": "accept",
"src": ["homelab"],
"dst": ["homelab:*"]
}
]
}
To expose your home LAN subnet to Tailscale clients (so you don't have to Tailscale every device):
# On a Linux machine in your LAN that's already a Headscale node
tailscale up \
--login-server https://headscale.yourdomain.com \
--advertise-routes=192.168.0.0/24 \
--authkey <KEY>Approve the route on the Headscale side
headscale routes list
headscale routes enable --route <ROUTE_ID>
For this, I use a small Beelink SER5 Pro running Ubuntu as a dedicated subnet router. Alternatively, you can use a GL.iNet Beryl AX router flashed with a Tailscale-aware firmware for the same job with zero Linux config.
Gotchas
DERP relay matters more than you think. WireGuard prefers direct UDP connections, but when both peers are behind symmetric NAT, they need a DERP relay server. Tailscale operates a global fleet of DERP servers β Headscale can (and should) use them. Your traffic doesn't go through them once a direct path is established, but without DERP configured, some peer connections simply won't form. I wasted two hours debugging this.
Magic DNS and split DNS. Headscale's MagicDNS pushes a resolver to clients that resolves addresses. This works well, but if you already run Pi-hole or AdGuard Home on your network, you may get conflicts. Solution: set your local DNS server as a nameserver in Headscale's config and let it handle all resolution. A Raspberry Pi 5 with Pi-hole and Headscale in separate containers handles this cleanly.
Upgrades break things sometimes. Headscale's API is not yet 1.0 stable. When the Tailscale client on your phones and laptops auto-updates, it can stop talking to older Headscale versions. Pin Headscale versions in production and test upgrades in a spare container first. I keep a SanDisk 128GB USB drive with a known-good Headscale binary as a recovery option.
Port 443 conflicts. If you're running Nginx Proxy Manager or Caddy as a reverse proxy on the same host, you can't also have Headscale bind 443. Options: (a) put Headscale on a dedicated IP; (b) use a separate LXC with its own IP just for Headscale; (c) terminate TLS at the reverse proxy and pass HTTP to Headscale on an internal port β but note Headscale requires special proxy headers for this to work correctly.
Connecting It to Your Existing Proxmox Setup
If you've already got K3s or other services running across Proxmox nodes, adding them to Headscale is straightforward. Install the Tailscale client on each Proxmox node (yes, Proxmox itself runs Tailscale fine), then join with a pre-auth key:
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up \
--login-server https://headscale.yourdomain.com \
--authkey <KEY> \
--hostname proxmox-node-1
Now your Proxmox nodes have stable, routable Tailscale IPs that work regardless of what your LAN DHCP assigns. This pairs particularly well with Longhorn for K3s persistent storage, where nodes need reliable peer addresses.
The Bigger Picture
Headscale is stable enough for daily home lab use. The project has been actively maintained since 2021, has a solid community on GitHub, and the architecture is straightforward β SQLite for state, a REST API for management, and the Tailscale-compatible coordination protocol.
The main thing you give up vs. managed Tailscale: the polished admin UI (Headscale has a CLI and a community-built web UI called Headscale-UI), the mobile app login flow, and Tailscale's own DERP server management if you ever want to run private DERP nodes.
What you gain: full control, no external dependencies for your mesh VPN, and a genuinely interesting networking project. For a home lab running on a Beelink EQ12 or a proper rack unit, it's a weekend project that pays dividends every time you SSH into something from a coffee shop without thinking about it.
---
Disclosure: This post contains affiliate links. If you purchase through these links, I may earn a small commission at no extra cost to you.
