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.
146 lines
5 KiB
YAML
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
|