Headscale on Proxmox LXC: Self-Hosting Your Own Tailscale Control Server
Self-Hosting

Headscale on Proxmox LXC: Self-Hosting Your Own Tailscale Control Server

Ricardo Gil
April 13, 2026
7 min read
#Headscale #Tailscale #Proxmox #Self-Hosting #Home Lab #Networking #VPN #LXC

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.

bash
# 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:

  • CPU: 1–2 cores
  • RAM: 256MB (512MB to be comfortable)
  • Disk: 8GB (Headscale's SQLite DB stays small)
  • Network: bridged to your main LAN bridge (vmbr0)
  • Nesting: enabled (required for some network operations)
  • Via the Proxmox UI: Create CT β†’ select template β†’ set resources β†’ under Options, enable "Nesting" β†’ start the container.

    Or via CLI:

    bash
    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:

    bash
    # 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.

    bash
    nano /etc/headscale/config.yaml

    Key fields to set:

    yaml
    # The public FQDN your clients will connect to
    server_url: https://headscale.yourdomain.com

    Address and port Headscale listens on

    listen_addr: 0.0.0.0:8080 metrics_listen_addr: 127.0.0.1:9090

    TLS β€” 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-01

    If using Let's Encrypt, Headscale needs to bind 443 directly

    listen_addr: 0.0.0.0:443

    DERP β€” use Tailscale's public DERP servers or run your own

    derp: urls: - https://controlplane.tailscale.com/derpmap/default auto_update_enabled: true

    IP range for your Tailnet

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

    DNS 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:

    yaml
    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:

    bash
    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):

    bash
    headscale users create homelab
    headscale users list

    To connect a device, generate a pre-auth key:

    bash
    # One-time use key, expires in 24 hours
    headscale preauthkeys create --user homelab --expiration 24h

    Reusable 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:

    bash
    # 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:

    bash
    headscale nodes list

    ACLs and Routes

    This is where Headscale really earns its keep. You can define fine-grained ACLs in HuJSON:

    bash
    nano /etc/headscale/acls.yaml

    json
    {
      "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):

    bash
    # 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 .home.yourdomain.com 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:

    bash
    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.

    πŸ“¬Weekly Newsletter

    Get the best home lab & AI content

    No spam. One email per week. Unsubscribe anytime.

    Share this article