diff --git a/dankclaw/Caddyfile b/dankclaw/Caddyfile index 673e1de..44db366 100644 --- a/dankclaw/Caddyfile +++ b/dankclaw/Caddyfile @@ -23,7 +23,11 @@ # resolves these names to 192.168.0.143. { - admin off + # Admin stays ON: it binds to 127.0.0.1:2019 inside the container and the + # port is not published, so nothing outside the container can reach it. + # It powers the config file watcher, so a rendered Caddyfile change + # hot-reloads without a container restart. 'admin off' once left a broken + # basicauth hash sitting on disk unapplied for hours (2026-08-22). email admin@franksdank.cloud # Solve ACME challenges for every site via Porkbun DNS (no port 80/443 diff --git a/docs/Infrastructure/Caddy.md b/docs/Infrastructure/Caddy.md index 87feebb..db0f227 100644 --- a/docs/Infrastructure/Caddy.md +++ b/docs/Infrastructure/Caddy.md @@ -73,10 +73,17 @@ Validate before a reload: caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile ``` +The admin endpoint stays on (loopback-only, unpublished port), so a Caddyfile change hot-reloads within seconds via the file watcher. deploy.sh also issues an explicit reload after rendering. Manual reload from the host: + +``` +podman exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile +``` + Certificates and ACME state live in the caddy data volume. Back it up with the stack. Each vhost owns one certificate; renewals are automatic and also use DNS-01. ## Gotchas +- `admin off` disables the config file watcher: rendered changes then sit on disk while Caddy serves its stale in-memory config until the container is recreated, and nothing logs an error (2026-08-22: a broken basicauth hash survived several deploys this way). `tests/caddyfile.bats` forbids `admin off`. - The basicauth template must stay a bare `REPLACE_WITH_HASH` token (no `$2a$14$` prefix) — the rendered value is the complete hash. A double prefix silently rejects every password with 401; no Caddy error is logged. - A new vhost needs three things: a dnsmasq `addn-hosts` line, a Porkbun A record, and the Caddyfile block. See [[LAN-DNS]]. - The G36 has no NAT loopback, so LAN clients must resolve the name to the LAN IP. That is the dnsmasq role in [[LAN-DNS]]. diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 3bfd813..16e01fe 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -155,6 +155,18 @@ if [ '${COMPOSE_UP[$TARGET]}' = '1' ]; then fi done podman-compose -f '${COMPOSE[$TARGET]}' up -d + + # A Caddyfile render is a bind-mount content change, not a container + # config change: the caddy container often survives 'up -d' and keeps + # serving its in-memory config. Reload explicitly; the file watcher + # (admin on) is the backstop if the API is not up yet. + for i in 1 2 3 4 5; do + if podman exec caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile >/dev/null 2>&1; then + echo \" Caddy reloaded.\" + break + fi + sleep 3 + done else echo '[4/4] Skipping compose restart (stack runs as bare containers on this host)' fi diff --git a/tests/caddyfile.bats b/tests/caddyfile.bats index f33e909..fd34a4f 100644 --- a/tests/caddyfile.bats +++ b/tests/caddyfile.bats @@ -49,6 +49,19 @@ CADDYFILE="$REPO_ROOT/dankclaw/Caddyfile" grep -q 'REPLACE_WITH_HASH' "$CADDYFILE" } +@test "the Caddyfile does not disable the admin endpoint (hot reload needs it)" { + # admin off disables Caddy's config file watcher: rendered Caddyfile + # changes are never loaded until the container is recreated, and the + # stale config fails silently (2026-08-22: basicauth stayed broken for + # hours across deploys). The admin API binds to 127.0.0.1:2019 inside + # the container and the port is not published, so it is not reachable + # from the LAN. + if grep -qE '^\s*admin\s+off' "$CADDYFILE"; then + echo "Caddyfile: 'admin off' disables hot reload of rendered config" + return 1 + fi +} + @test "the basicauth placeholder is a bare token (no bcrypt prefix in the template)" { # Regression (2026-08-22): the template line was # dank $2a$14$REPLACE_WITH_HASH diff --git a/tests/deployment_flow.bats b/tests/deployment_flow.bats index 21c6822..99614a5 100644 --- a/tests/deployment_flow.bats +++ b/tests/deployment_flow.bats @@ -37,6 +37,14 @@ load helpers # Regression: the Caddyfile render step in deploy.sh must survive a bcrypt # hash (which contains '/' characters). Reproduce the sed expression with a # slash-bearing hash and confirm the placeholder is replaced. +@test "deploy.sh reloads Caddy after rendering (hot reload, not just container start)" { + # The caddy container often survives 'up -d' (a Caddyfile change is a + # bind-mount content change, not a container config change). Without an + # explicit reload, a rendered fix can sit on disk while Caddy serves the + # stale in-memory config. + grep -q 'caddy reload' "$REPO_ROOT/scripts/deploy.sh" +} + @test "deploy.sh Caddyfile render survives a slash-bearing bcrypt hash" { local hash='$2a$14$dndiQ9EaycJv0w3Gp2oAYO/xH6O9x2ZRhMLJdCxrgi9rMn1te9D/spy' local rendered