Files
docker-compose-cookbooks/headscale
codeandClaude Opus 5.5 665c27e7f4 headscale: promote from _dev, split README into per-task docs
Move step-by-step guides into headscale/docs/, slim README to a quick
reference, and use $HEADSCALE_CONTAINER for docker exec commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 17:07:10 +00:00
..

Headscale

Overview

Self-hosted Tailscale control server. Reach homelab services (Jellyfin, Ollama, n8n, etc.) on a LAN node from your phone or laptop, anywhere — without exposing them to the internet.

[Phone / Laptop] → Tailscale client
        |
[Dedicated Server]  ← public IP, runs Headscale (this stack) behind NPM
        |
   WireGuard mesh (peer-to-peer when possible)
        |
[LAN Node]          ← Docker host on 192.168.x.x, joins as a Tailscale client

The dedicated server is only the control plane — traffic flows directly between devices whenever possible.

Project Details

Getting Started

Follow in order — each step has its own guide in docs/:

Step Task Where
1 Install Headscale Dedicated server
2 Configure Nginx Proxy Manager — HTTP/2 off, Websockets on NPM
3 Create a user and pre-auth key Dedicated server
4 Connect the LAN node LAN node
5 Connect phone / laptop Clients
6 Verify and troubleshoot Anywhere
7 Access LAN services (Jellyfin) Clients
8 Admin UI NPM + browser

Optional / maintenance:

Quick commands

# Set the headscale container name once per shell (server; change if your compose project name differs — check with `docker ps`)
export HEADSCALE_CONTAINER=headscale-headscale-1

# List all registered devices (server)
docker exec -it "$HEADSCALE_CONTAINER" headscale nodes list

# Create a one-hour pre-auth key for user ID 1 to join a new device (server)
docker exec -it "$HEADSCALE_CONTAINER" headscale preauthkeys create --user 1 --expiration 1h

# Create a 90-day API key for the admin UI (server)
docker exec -it "$HEADSCALE_CONTAINER" headscale apikeys create --expiration 90d

# Join this machine to the tailnet using a pre-auth key (client)
sudo tailscale up --login-server=https://headscale.example.com --authkey <KEY>

Environment Variable Notes

HEADSCALE_IMAGE – Headscale image (default: headscale/headscale:latest)
HEADSCALE_UI_IMAGE – Admin UI image (default: goodieshq/headscale-admin:latest)
HEADSCALE_RESTART / HEADSCALE_UI_RESTART – Restart policy (default: unless-stopped)
VOL_PATH – Base path for volumes (default: /data)
TZ – Timezone (default: America/Vancouver)
HEADSCALE_LOG_LEVEL – info, debug, etc.

# Documented for config.yaml (not read by compose):
HEADSCALE_DOMAIN – Public domain (e.g. headscale.example.com)
HEADSCALE_URL – Full client URL; must match server_url in config.yaml

Volume Notes

${VOL_PATH}/headscale/config → config.yaml (required before first start)
${VOL_PATH}/headscale/data   → SQLite database and state
${VOL_PATH}/headscale/logs   → Logs

Back up config/ and data/ regularly.

Network Notes

  • External proxy network plus private internal bridge
  • No published ports — all access via NPM
  • headscale → NPM proxy host, http://headscale:8080 (see NPM guide)
  • headscale-ui → Custom Location /admin on the same proxy host, http://headscale-ui:80 (a separate subdomain breaks via CORS — see Admin UI)
  • Firewall: dedicated server only needs 80/443 inbound; LAN node should allow UDP 41641 for direct WireGuard
  • Never forward 8080 (Headscale) or 8096 (Jellyfin) on any router

Docker Run

docker run -d \
  --name headscale \
  -v /data/headscale/config:/etc/headscale \
  -v /data/headscale/data:/var/lib/headscale \
  -v /data/headscale/logs:/var/log/headscale \
  headscale/headscale:latest \
  serve

Additional Notes / Gotchas

  • No valid config.yaml → Headscale won't start; server_url must match your public domain
  • NPM HTTP/2 Support must be off or tailscale up hangs (/ts2021 returns 500)
  • Pre-auth key expiration only limits new registrations — joined devices stay connected
  • SQLite by default (no external DB)
  • MagicDNS base domain is tailnet.local; change it in config.yaml if it clashes with your LAN DNS

Dockhand Stack, Deploy from Git

Cookbooks Repository stackname: headscale Compose file path: headscale/compose.yaml Additional env file (optional): headscale/sample.env

Then "Load" headscale/sample.env into the Environmental variables in dockhand.

Ensure config.yaml exists at ${VOL_PATH}/headscale/config/config.yaml before deploying.

Create the Stack, then configure the reverse proxy in Nginx Proxy Manager.