Files
truenas-stacks/headscale
2026-05-26 00:48:01 +00:00
..
2026-05-26 00:48:01 +00:00
2026-05-26 00:48:01 +00:00
2026-05-26 00:48:01 +00:00
2026-05-26 00:48:01 +00:00

Headscale pilot on PD

Self-hosted Headscale + Headplane pilot stack for replacing the Tailscale free-tier 3-user limit without doing a blind cutover.

Why this exists

  • remove the 3-human-user ceiling from the current Tailscale free-tier setup
  • keep the control plane on PD, the long-term primary Docker host
  • prove a small household-safe access model before expanding scope
  • keep the repo copy as the source of truth for stack config and policy

Pilot shape

  • Host: PlausibleDeniability
  • Live path: /mnt/docker-ssd/docker/compose/headscale
  • Services:
    • headscale
    • headplane
  • Data paths:
    • /mnt/docker-ssd/docker/appdata/headscale
    • /mnt/docker-ssd/docker/appdata/headplane
  • No OIDC on day one
  • No subnet-router or exit-node rollout on day one
  • No public Pangolin exposure on day one

URLs for the pilot

  • Headscale control plane: http://headscale.home.paccoco.com:8084
  • Headplane UI: http://headplane.home.paccoco.com:3005/admin

These are intentionally LAN/private-DNS endpoints for the pilot. If you later want Traefik/Pangolin exposure, use examples/traefik-routes.yml as a starting point instead of changing the pilot shape first.

Initial users

  • fizzlepoof
  • manndra

Policy references them as:

  • fizzlepoof@
  • manndra@

Initial tags

  • tag:infra
  • tag:apps
  • tag:admin

Access model

  • fizzlepoof@ = full pilot admin access to tag:infra, tag:apps, tag:admin, and his own devices
  • manndra@ = app access only via tag:apps plus her own devices
  • no limited-user tier yet
  • no routed-LAN access yet

Files

  • docker-compose.yaml
  • .env.example
  • config/headscale/config.yaml
  • config/headscale/policy.hujson
  • config/headplane/config.yaml
  • examples/traefik-routes.yml

Pre-deploy edits

Before first deploy, replace the placeholder values in:

  • .env copied from .env.example
  • config/headplane/config.yaml

Required changes:

  • replace CHANGE_ME_TO_EXACTLY_32_CHARS with a real 32-character cookie secret
  • confirm the hostnames/IP-backed DNS records exist in Technitium
  • if you want different ports or hostnames, update both the config files and .env

Deploy on PD

  1. Sync this directory into the live compose tree:
    • /mnt/docker-ssd/docker/compose/headscale
  2. Copy .env.example to .env
  3. Create persistent appdata paths:
    • /mnt/docker-ssd/docker/appdata/headscale
    • /mnt/docker-ssd/docker/appdata/headscale/run
    • /mnt/docker-ssd/docker/appdata/headplane
  4. Validate:
cd /mnt/docker-ssd/docker/compose/headscale
docker compose --env-file .env config
  1. Bring up Headscale first:
docker compose --env-file .env up -d headscale
docker logs headscale --tail=100
  1. Create pilot users:
docker exec -it headscale headscale users create fizzlepoof
docker exec -it headscale headscale users create manndra
docker exec -it headscale headscale users list
  1. Generate a Headscale API key for Headplane login:
docker exec -it headscale headscale apikeys create --expiration 90d

Save the returned key somewhere secure. Headplane uses it for the initial admin login.

  1. Enroll John's admin device.
  2. Bring up Headplane:
docker compose --env-file .env up -d headplane
docker logs headplane --tail=100
  1. Log in to Headplane with the API key from step 7.
  2. Register one tagged service node.
  3. Enroll one Manndra device.

Policy reload

After editing config/headscale/policy.hujson:

docker exec -it headscale kill -HUP 1

Then inspect the container logs for policy parse results.

What to validate before expanding

  • Headscale stays healthy
  • Headplane is usable enough to justify keeping it
  • John can reach tag:infra, tag:admin, and tag:apps
  • Manndra can reach tag:apps but not tag:infra or tag:admin
  • one tagged service node registers cleanly and behaves as expected
  • existing Tailscale remains intact as rollback during the pilot

Notes on Headplane integration

This stack enables Headplane's Docker integration so it can identify the Headscale container via the label:

  • me.tale.headplane.target=headscale

It also mounts the tracked Headscale config into Headplane so the UI can inspect and, if you later choose, manage more than just node registration.

Not done here on purpose

  • no OIDC wiring yet
  • no DERP customization yet
  • no public auth/proxy routing yet
  • no explicit subnet-router policy yet
  • no migration of the existing infrastructure stack's tailscale container yet

Pilot first. Cutover later.