Installation
Stand up headscale-admin - by hand, with Docker, Docker Compose, or Ansible. The same handful of decisions (URL, TLS, auth) apply to every method.
headscale-admin is a single binary that supervises headscale itself. There is no separate headscale to install: on
first start it downloads the pinned, checksum-verified headscale release, generates and owns headscale.yaml + the
sqlite DB / keys under work_dir, and is the only public listener in front of it.
Pick the method that fits you - they all configure the same binary:
- By hand - download the binary, write a config, run it under systemd.
- Docker - run the container directly with
docker run. - Docker Compose - a self-contained service with a persistent volume.
- Ansible - the bundled role: binary + config + systemd + optional headscale pre-config.
Prerequisites
- A Linux host (amd64 or arm64), or any OS that can run the binary - published builds cover
linuxanddarwin×amd64/arm64. - Outbound internet on first start - headscale-admin fetches the pinned headscale release from GitHub and verifies
it against the release checksums, then caches it. For air-gapped hosts you can point
headscale.binaryat a binary you supply. - A public address your Tailscale clients can reach (
headscale.public_url). headscale’s control plane and the admin UI are served from this one address. - A way to terminate TLS. Real Tailscale clients need HTTPS - headscale-admin can do it in-process
(
server.tls.mode: self | manual | acme) or you can front it with a TLS-terminating proxy (mode: none+server.behind_tls_proxy: true).
The decisions that matter
Every install boils down to the same few settings (the fully-commented reference config documents them all):
| Setting | What it controls |
|---|---|
headscale.public_url |
The client-facing control-server URL. Must match how clients actually connect (scheme + host + any non-default port). It becomes headscale’s server_url and the connect command in the UI. |
server.addr |
The single public listen address (default :3000; use :443 for a real deploy). |
server.tls.mode |
none (behind a proxy / local test), self (self-signed, IP-only/internal), manual (your cert + key), or acme (Let’s Encrypt, needs ports 80 + 443 reachable). |
auth |
disabled (everyone is admin - dev only), local email + password, or OIDC SSO. With auth on you must set auth.session_secret. |
headscale.work_dir |
Where headscale’s config, sqlite DB, keys, socket, and the downloaded binary live. This is the directory to persist and back up. |
Configuration & secrets
Config is a YAML file (--config <path>, default ./headscale-admin.yaml). Any setting can also be supplied as an
environment variable: prefix HSA_, uppercase the dotted path, and replace dots with underscores - so server.addr →
HSA_SERVER_ADDR and auth.oidc.client_id → HSA_AUTH_OIDC_CLIENT_ID. Env wins over the file, which is the tidy
way to inject secrets (the session secret, the OIDC client secret, the headscale-pf token) without writing them to
disk.
Generate the session secret (required when auth is enabled, ≥ 32 chars) with:
openssl rand -hex 32
First-run admin bootstrap
When authentication is enabled, headscale-admin refuses to start until at least one admin principal exists - so nobody is ever locked out of an open console. Create the first admin out-of-band:
headscale-admin admin create --config /path/to/config.yaml
- With OIDC disabled, it prompts for an email and password (8–72 characters).
- With OIDC enabled, it prompts for an email only - the matching same-email OIDC account then signs in as a
(pinned) admin. Alternatively, list bootstrap admins in
auth.oidc.admin_emailsand skip this step entirely.
headscale-admin admin list shows the current principals. (Each install guide below repeats the exact command for its
environment.)
What’s next
Head to the guide for your platform: By hand · Docker · Docker Compose · Ansible.