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 headscale release, generates and owns headscale.yaml.
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).
- Outbound internet on first start - headscale-admin fetches the headscale release from GitHub.
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).
Real Tailscale clients require HTTPS
The Tailscale app refuses a plain-HTTP control server. Let headscale-admin terminate TLS
(server.tls.mode: self | manual | acme) or put a TLS proxy in front (mode: none +
server.behind_tls_proxy: true). Plain HTTP (mode: none, no proxy) is only for local testing without real clients.
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. |
Match server.addr to the public_url port
Clients reach headscale at headscale.public_url, so its port must be the one actually served. When headscale-admin
terminates TLS itself, set server.addr to that same port - e.g. public_url: https://hs.example.com →
server.addr: ":443", or https://hs.example.com:8443 → server.addr: ":8443". Behind a TLS-terminating reverse
proxy (server.tls.mode: none, server.behind_tls_proxy: true) the proxy listens on the public_url port and
forwards to server.addr, so there the two ports may differ (public_url must still name the proxy’s public port).
Never run auth: disabled in production
With auth: disabled every visitor to the console has full admin access - it exists only for local development (it logs
a warning on every start). Any real deployment must use local accounts or OIDC and set auth.session_secret.
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.