Installation

Docker

Build the image and run headscale-admin as a single container with a persistent volume.

headscale-admin ships as one container that supervises headscale internally. A multi-arch image is published to ghcr.io/yousysadmin/headscale-admin (linux/amd64 + arm64), so you can docker run it directly - or build it yourself from a checkout. If you’d rather not hand-write docker run flags, the Docker Compose guide wraps all of this in one file.

The image is built from gcr.io/distroless/static-debian12:nonroot: it runs as the unprivileged user 65532, exposes 3000, and stores all state under the /data volume.

1. Get the image

Pull the published image (pin a release tag instead of latest for reproducible deploys):

docker pull ghcr.io/yousysadmin/headscale-admin:latest

Or build it yourself from a checkout of the repo:

make build-docker          # builds the frontend + linux binary, tags headscale-admin:dev

The rest of this guide uses the published image; swap in headscale-admin:dev if you built it locally.

2. Write the config

headscale-admin reads its config from /config.yaml inside the image (its default --config path). Put all state under /data so a single volume persists everything:

# config.yaml
server:
  addr: :3000
  db_path: /data/admin.db    # headscale-admin's own store (audit log, principals)
  tls:
    mode: none               # plain HTTP - front it with your own TLS proxy
    # For built-in Let's Encrypt instead (publish :80 + :443, see step 3):
    # mode: acme
    # acme:
    #   email: [email protected]
    #   hosts: [hs.example.com]
    #   http_addr: :80
    #   cache_dir: /data/certs

auth:
  disabled: false            # session_secret is supplied via the env var below
  session_ttl: 12h
  oidc:
    enabled: false

logging:
  level: info
  format: json
  output: stdout

headscale:
  work_dir: /data/headscale  # DB + headscale.yaml + keys + socket + downloaded binary
  # Client-facing control URL - MUST match how clients reach this service.
  public_url: https://hs.example.com

3. Run it

docker volume create headscale-data

docker run -d --name headscale-admin --restart unless-stopped \
  -p 3000:3000 \
  -v "$PWD/config.yaml:/config.yaml:ro" \
  -v headscale-data:/data \
  -e HSA_AUTH_SESSION_SECRET="$(openssl rand -hex 32)" \
  -e HSA_AUTH_BOOTSTRAP_ADMIN_EMAIL="[email protected]" \
  -e HSA_AUTH_BOOTSTRAP_ADMIN_PASSWORD="change-me-min-8-chars" \
  --stop-timeout 30 \
  ghcr.io/yousysadmin/headscale-admin:latest
  • Use a named volume (as above), not a host bind mount, for /data: the image runs as uid 65532 and seeds the volume with the right ownership. A bind mount would need a manual chown -R 65532:65532.
  • The first start needs outbound internet to download + verify the pinned headscale binary (cached on the volume afterwards).
  • --stop-timeout 30 gives the control plane time to drain so peers reconnect cleanly.

Built-in TLS (ACME)

To let headscale-admin terminate TLS itself, set tls.mode: acme (above) and publish the standard ports so Let’s Encrypt’s HTTP-01 challenge and HTTPS both reach it:

  -p 443:3000 -p 80:80 \

Otherwise keep mode: none and put a TLS-terminating reverse proxy in front (add server.behind_tls_proxy: true to the config so cookies are marked Secure).

4. Create the first admin

With auth enabled the admin surface needs at least one admin, so the -e HSA_AUTH_BOOTSTRAP_ADMIN_* vars in step 3 seed one on first start: serve creates a pinned super-admin with those credentials if no account with that email exists yet (email-only when OIDC is enabled - drop the password). It’s idempotent and never overwrites an existing account, so you can leave the vars set or drop them on the next docker run.

Then open /admin on your published address and sign in (email + password, or the matching OIDC account).

Changing the bootstrap password later won’t reset an existing admin - use the Admins page in the UI. The CLI headscale-admin admin create also exists for non-container installs, but it must run while the server is stopped (it opens the same database, which serve holds locked).

Logs, upgrades, backups

docker logs -f headscale-admin
  • Upgrade headscale-admin - docker pull a newer tag (or rebuild) and recreate the container; /data is preserved by the volume.
  • Upgrade headscale - from the Settings page in the UI (pinned + checksum-verified; don’t swap it by hand).
  • Back up - the headscale-data volume holds everything, or use the Backup page for consistent exports.