all writing

replacing tailscale with headscale

tailscale's free tier is generous but i'd rather own the control plane. headscale is the self-hosted replacement and it runs fine on proxmox.

i used Tailscale to access my homelab. Proxmox box, k3s cluster on top, a few services (WFD 15 covers the stack). install the client, authenticate, everything gets a stable IP, MagicDNS gives names. painless.

i didn't have a strong reason to replace it. the free tier covers 100 devices and 3 users which is more than i'll ever need. the control plane still knows every device on my network though, their IPs, their keys, and which subnets they can reach. i already had a box that could run this myself, so i did.

Headscale is a self-hosted, open source implementation of the Tailscale control plane. 35.9k stars, BSD-3 license, v0.28.0 as of Feb 2026. one of the maintainers works at Tailscale and is allowed to work on Headscale during business hours, which is about as close to an endorsement as you'll get from an upstream vendor for an alternative implementation.

what headscale is

Tailscale has two parts: the client (open source, runs on your devices) and the control plane (proprietary, hosted by Tailscale). the control plane handles key exchange, peer discovery, NAT traversal, DNS, and ACL enforcement. the clients do the WireGuard tunneling peer-to-peer.

Headscale replaces the control plane. the clients stay the same. you point tailscale up --login-server https://headscale.example.com at your instance and everything else works.

Loading diagram...
Loading diagram...

traffic goes peer-to-peer between clients. the control plane only handles discovery. if NAT traversal fails, traffic routes through DERP relays (Headscale can embed its own).

feature parity

what i used from Tailscale and whether Headscale covers it:

feature Headscale
MagicDNS yes, split DNS and extra records
subnet routers yes, with auto-approvers in ACL policy
exit nodes yes
ACLs yes, full Tailscale ACL engine with autogroups
OIDC (Google login) yes, OIDC groups can't be used in ACLs though
Taildrop (file sharing) yes
Tailscale SSH yes
embedded DERP server yes
Funnel / Serve no, i use Cloudflare Tunnel for this anyway
network flow logs no
multiple tailnets no, single tailnet by design

none of the missing stuff matters for a homelab.

where it runs

it needs to be reachable for devices to find each other. if it lives inside the homelab and Proxmox goes down, nothing can reconnect. a $5 VPS would fix that, and i only have one site, so if Proxmox is down i can't reach anything regardless. LXC container on Proxmox it is.

LXC setup

Headscale is a single Go binary with no kernel dependencies, so a Debian LXC with 128MB RAM runs it fine.

bash
wget https://github.com/juanfont/headscale/releases/download/v0.28.0/headscale_0.28.0_linux_amd64.deb
dpkg -i headscale_0.28.0_linux_amd64.deb
systemctl enable --now headscale

config at /etc/headscale/config.yaml:

yaml
server_url: https://headscale.pixie.local
listen_addr: 0.0.0.0:8080
noise:
  private_key_path: /var/lib/headscale/noise_private.key
prefixes:
  v4: 100.64.0.0/10
  allocation: sequential
dns:
  base_domain: tail.home
  magic_dns: true
  nameservers:
    global:
      - 1.1.1.1
  extra_records:
    - name: "grafana.tail.home"
      type: "A"
      value: "100.64.0.3"
    - name: "pixie.tail.home"
      type: "A"
      value: "100.64.0.1"
database:
  type: sqlite
  sqlite:
    path: /var/lib/headscale/db.sqlite

i already had a Caddy LXC handling TLS for other services so that sits in front.

routing into k8s

pod CIDR is 10.42.0.0/16, service CIDR is 10.43.0.0/16. the same cluster running the stuff from WFD 13 and WFD 21. to reach pods and services from my laptop over the mesh, one of the k3s nodes advertises those subnets as routes.

bash
# may@aurora
sudo tailscale up --login-server https://headscale.pixie.local \
  --advertise-routes=10.42.0.0/16,10.43.0.0/16

approve the routes:

bash
headscale routes list
headscale routes enable -r <route-id>

or set up auto-approval in the ACL policy:

json
{
  "tagOwners": {
    "tag:k8s": ["may@"]
  },
  "autoApprovers": {
    "routes": {
      "10.42.0.0/16": ["tag:k8s"],
      "10.43.0.0/16": ["tag:k8s"]
    }
  },
  "acls": [
    { "action": "accept", "src": ["*"], "dst": ["*:*"] }
  ]
}

after this, kubectl works from my laptop without port forwarding or a VPN gateway and any device on the mesh can hit k8s services by cluster IP.

terraform

there's a Terraform provider: awlsring/headscale. v0.5.0 supports Headscale v0.28.x.

hcl
terraform {
  required_providers {
    headscale = {
      source  = "awlsring/headscale"
      version = "0.5.0"
    }
  }
}

provider "headscale" {
  api_key  = var.headscale_api_key
  endpoint = "https://headscale.pixie.local"
}

resource "headscale_user" "may" {
  name = "may"
}

resource "headscale_pre_auth_key" "k8s_nodes" {
  user            = headscale_user.may.name
  reusable        = true
  ephemeral       = false
  time_to_expire  = "8760h"
  acl_tags        = ["tag:k8s"]
}

the pre-auth key automates node registration. i pass it to cloud-init when provisioning k3s VMs so they join the mesh on boot.

i manage my homelab with OpenTofu and the Proxmox provider already. adding the Headscale provider means users, keys, and ACL policies live alongside the VM definitions. one tofu apply and a node exists, is registered, and has its routes approved. that felt good the first time i ran it.

was it worth it

arguments against:

  • Tailscale worked and i added maintenance for no functional gain
  • Headscale has no web UI (third-party options exist, they're separate projects)
  • debugging connection issues is harder without Tailscale's admin console

arguments for:

  • i own the coordination data
  • one less external dependency
  • the Terraform provider lets me manage it as code alongside everything else
  • i learned more about how Tailscale actually works under the hood