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 uid65532and seeds the volume with the right ownership. A bind mount would need a manualchown -R 65532:65532. - The first start needs outbound internet to download + verify the pinned headscale binary (cached on the volume afterwards).
--stop-timeout 30gives 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 createalso exists for non-container installs, but it must run while the server is stopped (it opens the same database, whichserveholds locked).
Logs, upgrades, backups
docker logs -f headscale-admin
- Upgrade headscale-admin -
docker pulla newer tag (or rebuild) and recreate the container;/datais 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-datavolume holds everything, or use the Backup page for consistent exports.