Self-host Headscale with a protected web UI
Deploy Headscale behind Traefik and protect the optional web UI with Authentik while keeping the control API reachable.
Headscale is an open-source control server for Tailscale-compatible WireGuard networks. It is a great homelab tool because it lets you manage private devices without depending entirely on a third-party coordination server.
In this setup:
headscale.<your-domain>exposes the Headscale control endpoint.headscale-ui.<your-domain>exposes a browser UI.- Traefik handles TLS.
- Authentik protects only the UI.
- The Headscale API remains available for clients.
Folder layout
1
2
3
4
5
6
/home/ubuntu/headscale/
├── docker-compose.yml
├── .env
├── config/
│ └── config.yaml
└── lib/
This keeps the service self-contained and easy to back up.
Compose file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
services:
headscale:
image: headscale/headscale:latest
container_name: headscale
restart: unless-stopped
command: serve
volumes:
- ./config:/etc/headscale
- ./lib:/var/lib/headscale
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.headscale-secure.entrypoints=https"
- "traefik.http.routers.headscale-secure.rule=Host(`headscale.<your-domain>`)"
- "traefik.http.routers.headscale-secure.tls=true"
- "traefik.http.routers.headscale-secure.tls.certresolver=cloudflare"
- "traefik.http.routers.headscale-secure.service=headscale"
- "traefik.http.services.headscale.loadbalancer.server.port=8080"
headscale-ui:
image: ghcr.io/gurucomputing/headscale-ui:latest
container_name: headscale-ui
restart: unless-stopped
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.headscale-ui-secure.entrypoints=https"
- "traefik.http.routers.headscale-ui-secure.rule=Host(`headscale-ui.<your-domain>`)"
- "traefik.http.routers.headscale-ui-secure.middlewares=authentik@docker"
- "traefik.http.routers.headscale-ui-secure.tls=true"
- "traefik.http.routers.headscale-ui-secure.tls.certresolver=cloudflare"
- "traefik.http.routers.headscale-ui-secure.service=headscale-ui"
- "traefik.http.services.headscale-ui.loadbalancer.server.port=8080"
networks:
proxy:
external: true
The important detail is the UI port: many people assume it is 80, but this UI listens on 8080 internally.
Minimal Headscale config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
server_url: https://headscale.<your-domain>
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
private_key_path: /var/lib/headscale/private.key
noise:
private_key_path: /var/lib/headscale/noise_private.key
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
unix_socket: /var/run/headscale/headscale.sock
log:
level: info
policy:
mode: database
Your real config can include DNS, DERP, ACLs, and OIDC later.
Create users
1
2
3
cd /home/ubuntu/headscale
docker compose exec headscale headscale users create nox
docker compose exec headscale headscale users create laptop
Register a node
On the client:
1
tailscale up --login-server https://headscale.<your-domain>
On the server:
1
2
3
docker compose exec headscale headscale nodes register \
--user nox \
--key <machine-key-from-client>
Rename a node:
1
docker compose exec headscale headscale nodes rename <node-id> <new-name>
Protect only the UI
The Headscale control server is used by Tailscale clients. Do not put an interactive browser SSO flow in front of it unless you know exactly how your clients authenticate.
Protect the UI instead:
1
- "traefik.http.routers.headscale-ui-secure.middlewares=authentik@docker"
And add an Authentik outpost route:
1
- "traefik.http.routers.authentik-headscale-ui-outpost.rule=Host(`headscale-ui.<your-domain>`) && PathPrefix(`/outpost.goauthentik.io/`)"
Health checks
1
curl https://headscale.<your-domain>/health
Expected:
1
{"status":"pass"}
Check UI auth:
1
curl -I https://headscale-ui.<your-domain>/web
Expected:
1
2
HTTP/2 302
location: https://auth.<your-domain>/application/o/authorize/...
Lessons learned
- Keep Headscale and Headscale UI separate in your mental model.
- The control endpoint is for clients; the UI is for humans.
- Authentik is perfect for the UI.
Traefik labels are enough; no extra public ports are needed.
Related documentation
These posts connect to this topic and help build the bigger homelab picture:
- Private Pi-hole DNS over Headscale with DNSCrypt and Authentik — builds on Headscale by making Pi-hole available privately over the tailnet.
- Build a homelab auth gateway with Traefik and Authentik — explains the reusable Authentik forward-auth pattern behind the protected UI.
- Self-host OpenClaw with Docker, Traefik, Authentik, and Telegram — uses the same protected-service approach for an AI gateway.