central-proxy/headscale/config.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

146 lines
5 KiB
YAML

# Headscale coordination server for the owner's self-hosted tailnet, reached at
# https://remote.waldson.com.br through the central Caddy proxy. This file is
# the committed posture: MagicDNS, the embedded DERP relay, and the SQLite data
# layout are all reproduced on every recreate instead of depending on runtime
# state. Only the SQLite database and the auto-generated private keys (under
# /var/lib/headscale, the git-ignored bind mount) are mutable at runtime.
#
# Schema tracks the pinned image (headscale/headscale:v0.29). When bumping the
# minor tag, diff this against that release's config-example.yaml: Headscale's
# config schema changes between minors.
# Public login URL every client connects to. Caddy terminates TLS for this
# hostname and reverse-proxies to listen_addr below. DERP requires https here.
server_url: https://remote.waldson.com.br
# Bind on all interfaces inside the container so Caddy can reach it by name on
# proxy_net (headscale-headscale-1:8080). The port is never published to the
# host; only Caddy on 80/443 fronts it, the same shape as the other stacks.
listen_addr: 0.0.0.0:8080
# Metrics and gRPC stay bound to loopback inside the container and are never
# published. Administration is done with `docker exec ... headscale` over the
# unix socket below, so the gRPC remote-CLI surface is not exposed.
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false
# Caddy is the only thing in front of Headscale; trust its forwarding headers so
# logs and rate limiting see the real client IP. Caddy reaches Headscale from
# its own container on proxy_net, so trust that private range.
trusted_proxies:
- 172.16.0.0/12
noise:
private_key_path: /var/lib/headscale/noise_private.key
# Tailnet address pools. Left at the standard Tailscale CGNAT/ULA ranges.
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
allocation: sequential
# Embedded DERP relay: run our own relay and STUN so even traffic that cannot go
# direct is relayed through this droplet, never through Tailscale Inc. The
# upstream DERP map is dropped (urls: [] and no auto-update) so the tailnet uses
# only this region.
derp:
server:
enabled: true
region_id: 999
region_code: "waldson"
region_name: "Waldson Embedded DERP"
verify_clients: true
# STUN for NAT traversal. Published on the host as 3478/udp (the one new
# firewall row this stack adds); mandatory when the embedded DERP is on.
stun_listen_addr: "0.0.0.0:3478"
private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true
# Droplet public IPv4 so clients can reach this DERP region directly for
# better connection stability. No IPv6 line: the droplet has no stable
# public IPv6, and advertising a bogus one would send clients to a dead
# address.
ipv4: 174.138.36.232
# No external DERP maps: relayed traffic must stay on our own infrastructure.
urls: []
paths: []
auto_update_enabled: false
update_frequency: 3h
disable_check_updates: false
node:
expiry: 0
ephemeral:
inactivity_timeout: 30m
# HA subnet-router health probing. Kept on so that when the Home Assistant
# box (primary) and mercurio (failover) both advertise 192.168.0.0/24, the
# primary role fails over to whichever is healthy. This is the route-failover
# support the tailnet topology depends on.
routes:
ha:
probe_interval: 10s
probe_timeout: 5s
database:
type: sqlite
debug: false
gorm:
prepare_stmt: true
parameterized_queries: true
skip_err_record_not_found: true
slow_threshold: 1000
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
wal_autocheckpoint: 1000
# TLS is terminated by Caddy, so Headscale serves plain HTTP on listen_addr and
# manages no certificates of its own.
tls_letsencrypt_hostname: ""
tls_letsencrypt_challenge_type: HTTP-01
tls_cert_path: ""
tls_key_path: ""
log:
level: info
format: text
policy:
# Allow-all within the tailnet: single-user fleet, the tailnet boundary is the
# security boundary. An empty path keeps the built-in allow-all default.
mode: file
path: ""
# MagicDNS so devices reach each other by name (ssh mercurio). The base domain
# is deliberately different from server_url's host, which Headscale requires,
# and exists only inside the tailnet (no public records for it).
dns:
magic_dns: true
base_domain: ts.waldson.com.br
override_local_dns: true
nameservers:
global:
- 1.1.1.1
- 1.0.0.1
- 2606:4700:4700::1111
- 2606:4700:4700::1001
split: {}
search_domains: []
extra_records: []
# CLI connects over this unix socket with no auth. Relocated from the default
# /var/run/headscale (root-owned, not writable by our non-root container) into
# the bind-mounted data dir, so the UID 1000 process can create it and
# `docker exec ... headscale` works.
unix_socket: /var/lib/headscale/headscale.sock
unix_socket_permission: "0770"
logtail:
enabled: false
taildrop:
enabled: true
auto_update:
enabled: false