# 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: ```bash 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: ```bash docker compose config --quiet docker compose up -d headscale traefik ``` Create an API key for Headplane: ```bash 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: ```bash 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 ```bash 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: ```bash 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 ```bash 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.