headscale_lht/README.md

3.5 KiB

Headscale + Headplane + Traefik

This repository contains privacy-safe example files for running a self-hosted Headscale control plane with the Headplane web UI behind Traefik and automatic Let's Encrypt certificates.

Only README.md, .gitignore, and files ending in .example are published. Real configuration, credentials, databases, generated keys, certificates, backups, and container images are ignored by design.

Architecture

  • Headscale provides the Tailscale-compatible control plane and embedded DERP/STUN.
  • Headplane provides the administration UI at /admin.
  • Traefik terminates HTTPS and routes requests to Headscale or Headplane.

The example pins Headscale v0.29.2, Headplane 0.7.0, and Traefik v3.7.9.

Requirements

  • A Linux server with Docker Engine and Docker Compose v2
  • A public IPv4 address
  • A domain such as headscale.example.com pointing to that address
  • Inbound 80/tcp, 443/tcp, and 3478/udp allowed by the firewall

Prepare configuration

Clone the repository, then create the ignored runtime files from the examples:

cp docker-compose.yml.example docker-compose.yml
cp headscale/config/config.yaml.example headscale/config/config.yaml
cp headscale/config/acl.hujson.example headscale/config/acl.hujson
cp headplane/config/config.yaml.example headplane/config/config.yaml
cp traefik/traefik.yml.example traefik/traefik.yml
cp traefik/dynamic/headscale.yml.example traefik/dynamic/headscale.yml

mkdir -p headscale/data headscale/run headplane/data traefik/letsencrypt
touch traefik/letsencrypt/acme.json
chmod 600 traefik/letsencrypt/acme.json

Replace the example values in the copied files:

  • headscale.example.com: the public control-plane domain
  • dns.example.com: the MagicDNS base domain
  • 203.0.113.10: the server's public IPv4 address
  • admin@example.com: the Let's Encrypt account email
  • REPLACE_WITH_OPENSSL_RAND_HEX_32: output of openssl rand -hex 32

Do not put real credentials in a file ending in .example.

Start and configure

Validate the Compose model, then start Headscale and Traefik first:

docker compose config --quiet
docker compose up -d headscale traefik

Create an API key for Headplane:

docker compose exec headscale headscale apikeys create --expiration 365d

Set the resulting key as headscale.api_key in the ignored headplane/config/config.yaml, set a random cookie secret, and start Headplane:

docker compose up -d headplane
docker compose ps

Headscale is served at https://headscale.example.com; Headplane is served at https://headscale.example.com/admin.

Add a user and client

docker compose exec headscale headscale users create default
docker compose exec headscale headscale preauthkeys create \
  --user default --reusable --expiration 24h

Join a client with the generated pre-auth key:

tailscale up \
  --login-server https://headscale.example.com \
  --auth-key YOUR_PREAUTH_KEY

Optional OIDC

The Headscale and Headplane examples include commented OIDC sections. Copy and uncomment them only in the ignored runtime configuration files. Keep the OIDC client secret out of Git and use the same public issuer and client details in both services.

Operations

docker compose logs -f headscale headplane traefik
docker compose pull
docker compose up -d

Back up headscale/data, headplane/data, and traefik/letsencrypt securely. These directories contain private state and are intentionally never published.