Docker Compose
A self-contained Compose service that builds the image, persists everything to a named volume, and supervises headscale.
The repo ships a complete, ready-to-run Compose example at
examples/docker-compose/. By
default it pulls the published image, runs the container as nonroot, and keeps all state - sqlite DB, generated
headscale.yaml, keys, socket, and the downloaded headscale binary - on a named headscale-data volume, so recreating
the container keeps your network, nodes, and keys intact.
Quick start
cd examples/docker-compose
cp .env.example .env # then edit: set HSA_AUTH_SESSION_SECRET
$EDITOR config.docker.yaml # set headscale.public_url + your TLS mode
docker compose up -d # pulls ghcr.io/yousysadmin/headscale-admin
# open https://<your-host>/admin
Build from source instead? Comment out image: in docker-compose.yaml, uncomment the build: block, then
docker compose up -d --build (builds the frontend + binary end to end). --build is only needed the first time or
after a code change.
The three files
.env - the required secret plus the first-admin seed (Compose fails fast if HSA_AUTH_SESSION_SECRET is unset):
HSA_AUTH_SESSION_SECRET=<openssl rand -hex 32>
# First-admin bootstrap: seeded on first start (see "Create the first admin").
HSA_AUTH_BOOTSTRAP_ADMIN_EMAIL=[email protected]
HSA_AUTH_BOOTSTRAP_ADMIN_PASSWORD=<at least 8 chars>
# Only if you enable OIDC in config.docker.yaml:
# HSA_AUTH_OIDC_CLIENT_SECRET=...
config.docker.yaml - mounted read-only at /config.yaml (the image’s default --config path). The important knobs:
server:
addr: :3000
db_path: /data/admin.db
tls:
mode: none # plain HTTP behind your own TLS proxy, or:
# mode: acme # built-in Let's Encrypt (see the port note below)
# acme:
# email: [email protected]
# hosts: [hs.example.com]
# http_addr: :80
# cache_dir: /data/certs
auth:
disabled: false # session_secret comes from .env
oidc:
enabled: false
headscale:
work_dir: /data/headscale # everything persistent lives here (the volume)
public_url: https://hs.example.com # MUST match how clients connect
docker-compose.yaml - the service itself (abbreviated):
services:
headscale-admin:
image: ghcr.io/yousysadmin/headscale-admin:latest
# Build from source instead? Comment out `image:` and uncomment:
# build: { context: ../.., dockerfile: examples/docker-compose/Dockerfile }
restart: unless-stopped
ports:
- "3000:3000"
# Built-in ACME TLS instead? Publish the standard ports:
# - "443:3000"
# - "80:80"
environment:
HSA_AUTH_SESSION_SECRET: ${HSA_AUTH_SESSION_SECRET:?set in .env}
HSA_AUTH_BOOTSTRAP_ADMIN_EMAIL: ${HSA_AUTH_BOOTSTRAP_ADMIN_EMAIL:-}
HSA_AUTH_BOOTSTRAP_ADMIN_PASSWORD: ${HSA_AUTH_BOOTSTRAP_ADMIN_PASSWORD:-}
volumes:
- ./config.docker.yaml:/config.yaml:ro
- headscale-data:/data
stop_grace_period: 30s
volumes:
headscale-data:
First start needs outbound internet to download + verify the pinned headscale binary (cached on the volume afterwards). A named volume is used on purpose - the nonroot image (uid
65532) seeds/datawith the right ownership; a host bind mount would need a manualchown.
Create the first admin
With auth enabled, the admin surface needs at least one admin. The container seeds it for you on first start from the environment - no second command, and no clashing with the running server over the database lock:
# in .env
HSA_AUTH_BOOTSTRAP_ADMIN_EMAIL=[email protected]
HSA_AUTH_BOOTSTRAP_ADMIN_PASSWORD=<at least 8 chars> # omit when OIDC is enabled
serve creates a pinned super-admin with these credentials on startup if no account with that email exists yet. It’s
idempotent - it never overwrites an existing account, so you can leave the values in .env or remove them after the
first boot. Then sign in at /admin (email + password; with OIDC enabled the matching same-email IdP account signs in).
Changing
HSA_AUTH_BOOTSTRAP_ADMIN_PASSWORDlater does not reset an existing admin’s password - use the Admins page in the UI, oraccountself-service, for that.
Day-two
docker compose logs -f
docker compose pull && docker compose up -d # upgrade to a newer image
docker compose down # stop (the headscale-data volume persists)
(Building from source? Use docker compose up -d --build to pick up code changes instead.)
- Upgrade headscale from the Settings page (pinned + checksum-verified) - not by editing Compose.
- Back up the
headscale-datavolume, or use the Backup page for consistent exports.