central-proxy/headscale/compose.yaml
Waldson Patrício 6da5b3a8ab [009] Add Headscale stack behind Caddy at remote.waldson.com.br
Headscale (the self-hosted Tailscale control plane) runs as a new stack on
proxy_net, following the house pattern of syncthing/forgejo/ntfy: own
directory, own compose file, committed config, git-ignored bind-mounted state,
CLI-only admin via docker exec.

- headscale/config.yaml: server_url https://remote.waldson.com.br, MagicDNS
  with base domain ts.waldson.com.br, embedded DERP relay with the upstream
  Tailscale DERP map dropped so relayed traffic stays on the droplet, HA
  subnet-router route failover, SQLite state under /var/lib/headscale. The CLI
  unix socket is relocated into the data dir so the UID 1000 container can use
  it.
- headscale/compose.yaml: image pinned to the v0.29 minor, runs as 1000:1000,
  publishes only STUN on 3478/udp; gRPC not exposed.
- Caddyfile: remote.waldson.com.br reverse-proxies to headscale-headscale-1:8080.
- README: Headscale stack section (deploy, upgrade, user/node CLI runbook),
  firewall table row for 3478/udp, DNS row for remote.waldson.com.br.
2026-07-23 15:42:04 -03:00

50 lines
2.5 KiB
YAML

name: headscale
# Self-hosted Headscale coordination server (the open-source Tailscale control
# plane) for the owner's tailnet, behind the central Caddy proxy at
# remote.waldson.com.br. The HTTP API and the login/OIDC endpoints are reached
# only through Caddy on proxy_net; Caddy transparently upgrades the long-lived
# client control connections, the same as ntfy. The one thing published to the
# host is STUN on 3478/udp for the embedded DERP relay. Administration is
# CLI-only via `docker exec ... headscale` (see README.md for the deploy,
# upgrade, and user/node runbook). The whole posture lives in the committed
# config.yaml; only SQLite state and generated keys are mutable at runtime.
services:
headscale:
# Pinned to the current minor series. Headscale is pre-1.0, so the minor is
# the breaking-change axis: `pull` brings 0.29.x patches while a minor bump
# (0.30) stays a deliberate tag edit. Headscale trails new Tailscale client
# capabilities, so review compatibility before bumping the tag - if a client
# refuses to connect after an upgrade, version skew is the first suspect.
image: headscale/headscale:v0.29
# The image entrypoint is `headscale`; the server needs the `serve`
# subcommand (same shape as ntfy's `serve`).
command: serve
restart: unless-stopped
# Run as the droplet's deploy user so the SQLite db and generated private
# keys in the bind mount stay owned by 1000 and are manageable over plain
# SSH, matching the Syncthing and ntfy stacks. Headscale's default unix
# socket lives under root-only /var/run; config.yaml relocates it into the
# data dir so this non-root process can create it and the CLI still works.
user: "1000:1000"
volumes:
# Committed posture: server URL, MagicDNS, embedded DERP. Read-only.
- ./config.yaml:/etc/headscale/config.yaml:ro
# SQLite database, noise/DERP private keys, and the CLI socket live here on
# a host bind mount, so the instance survives recreation and is
# inspectable over SSH. Git-ignored via the repo's data/ rule.
- ./data:/var/lib/headscale
ports:
# STUN for the embedded DERP relay's NAT traversal. The relay itself and
# every control connection ride Caddy's existing 443; only this UDP port
# is new on the host firewall. The gRPC remote-CLI port is deliberately
# not published - admin is via docker exec over the unix socket.
- "3478:3478/udp"
mem_limit: 256m
networks:
- proxy_net
networks:
proxy_net:
external: true