caddy + pihole: make both boot-persistent and self-healing
Caddy left compose.yaml for a quadlet unit (caddy.service): joins dankclaw_default for container-name upstreams, Pull=never for the local-only image, Porkbun credentials via /home/dank/Containers/caddy/env written by deploy from .env, bare-IP-catch-all health check. deploy_caddy installs, enables, and starts the unit after retiring the bare container (state is in host volumes). Pi-hole: the repo now owns the sysctl drop-in (dankclaw/pihole/sysctl/99-dnsmasq.conf, ip_unprivileged_port_start=53); deploy_pihole installs it to /etc/sysctl.d/, stages conflicting later-sorted drop-ins in the home trash, and applies 'sysctl --system' (the boot code path). This fixes the 2026-08-26 crash loop (port-53 bind denied, restart counter 1645) caused by a second drop-in resetting the value to 80. Tests: new caddy_quadlet.bats seam; caddyfile/deployment_flow assertions repointed from the removed compose service to the quadlet; pihole.bats pins the sysctl drop-in and its deployment; test-changed.sh routes dankclaw/caddy/ and the pihole subtree to their seams. Docs: Caddy, Pi-hole, Dankclaw Stack, work log.
This commit is contained in:
1 parent
66b58a127b
commit
8660209c35
14 files changed
+248
-60
No files matched your search
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bats
|
||||
# Seam: canonical Caddy as a boot-persistent, self-healing quadlet on
|
||||
# dankclaw. It left compose.yaml on 2026-08-26 after a reboot exposed that a
|
||||
# bare compose container never comes back (rootless Podman has no boot-time
|
||||
# auto-start for bare containers; `restart: unless-stopped` does not survive
|
||||
# the explicit stop that shutdown performs). See AGENTS.md "Boot Persistence
|
||||
# & Self-Healing" and docs/Infrastructure/Caddy.md.
|
||||
|
||||
load helpers
|
||||
|
||||
CADDY_DIR="$REPO_ROOT/dankclaw/caddy"
|
||||
QUADLET="$CADDY_DIR/quadlet/caddy.container"
|
||||
ENV_EXAMPLE="$CADDY_DIR/env.example"
|
||||
|
||||
@test "the caddy quadlet container file exists" {
|
||||
[ -f "$QUADLET" ]
|
||||
}
|
||||
|
||||
@test "the caddy quadlet is installed for boot" {
|
||||
grep -q 'WantedBy=default.target' "$QUADLET"
|
||||
}
|
||||
|
||||
@test "the caddy quadlet restarts on failure (self-healing)" {
|
||||
grep -q '^Restart=always' "$QUADLET"
|
||||
grep -qE '^RestartSec=[1-9][0-9]*$' "$QUADLET"
|
||||
}
|
||||
|
||||
@test "the caddy quadlet publishes 80 and 443 to the LAN" {
|
||||
grep -qE '^PublishPort=0\.0\.0\.0:80:80/tcp$' "$QUADLET"
|
||||
grep -qE '^PublishPort=0\.0\.0\.0:443:443/tcp$' "$QUADLET"
|
||||
}
|
||||
|
||||
@test "the caddy quadlet joins the compose network for container-name upstreams" {
|
||||
# The Caddyfile reverse-proxies to container names (authentik-server,
|
||||
# grafana, ...); those resolve only on the shared bridge.
|
||||
grep -q '^Network=dankclaw_default$' "$QUADLET"
|
||||
}
|
||||
|
||||
@test "the caddy quadlet never pulls the local-only image at boot" {
|
||||
grep -q '^Pull=never$' "$QUADLET"
|
||||
}
|
||||
|
||||
@test "the caddy quadlet reads its env file and mounts its state" {
|
||||
grep -q '^EnvironmentFile=/home/dank/Containers/caddy/env$' "$QUADLET"
|
||||
grep -qE '^Volume=/home/dank/Containers/caddy/Caddyfile:/etc/caddy/Caddyfile:ro$' "$QUADLET"
|
||||
grep -qE '^Volume=/home/dank/Containers/caddy/data:/data$' "$QUADLET"
|
||||
grep -qE '^Volume=/home/dank/Containers/caddy/config:/config$' "$QUADLET"
|
||||
grep -qE '^Volume=/srv/homepage:/srv/homepage:ro$' "$QUADLET"
|
||||
}
|
||||
|
||||
@test "the caddy env example has placeholder values only" {
|
||||
[ -f "$ENV_EXAMPLE" ]
|
||||
grep -q '^PORKBUN_API_KEY=changeme$' "$ENV_EXAMPLE"
|
||||
grep -q '^PORKBUN_API_SECRET_KEY=changeme$' "$ENV_EXAMPLE"
|
||||
}
|
||||
|
||||
@test "no secret material in the caddy files" {
|
||||
local f
|
||||
while IFS= read -r f; do
|
||||
has_no_leaked_secret "$f"
|
||||
has_no_leaked_sudo_pw "$f"
|
||||
done < <(find "$CADDY_DIR" -type f | sort)
|
||||
}
|
||||
+18
-10
@@ -76,13 +76,15 @@ CADDYFILE="$REPO_ROOT/dankclaw/Caddyfile"
|
||||
}
|
||||
|
||||
@test "compose no longer defines the quadlet-replaced services" {
|
||||
# gitea, its postgres, and open-webui run as boot-persistent quadlets on
|
||||
# dankclaw (see dankclaw/gitea/quadlet, dankclaw/open-webui/quadlet).
|
||||
! grep -qE '^\s+(gitea|postgres|openwebui):\s*$' "$REPO_ROOT/dankclaw/compose.yaml"
|
||||
# gitea, its postgres, open-webui, and caddy run as boot-persistent
|
||||
# quadlets on dankclaw (see dankclaw/gitea/quadlet,
|
||||
# dankclaw/open-webui/quadlet, dankclaw/caddy/quadlet).
|
||||
! grep -qE '^\s+(gitea|postgres|openwebui|caddy):\s*$' "$REPO_ROOT/dankclaw/compose.yaml"
|
||||
}
|
||||
|
||||
@test "the caddy service healthcheck probes the bare-IP catch-all" {
|
||||
grep -q 'curl.*-sf.*http://localhost:80/' "$REPO_ROOT/dankclaw/compose.yaml"
|
||||
@test "the caddy quadlet healthcheck probes the bare-IP catch-all" {
|
||||
grep -q 'HealthCmd=curl -sf -o /dev/null http://localhost:80/' \
|
||||
"$REPO_ROOT/dankclaw/caddy/quadlet/caddy.container"
|
||||
}
|
||||
|
||||
@test "plex vhosts proxy to PMS over a TLS upstream (PMS drops plain HTTP off-loopback)" {
|
||||
@@ -138,13 +140,19 @@ CADDYFILE="$REPO_ROOT/dankclaw/Caddyfile"
|
||||
grep -q 'redir https://franksdank.cloud/' "$CADDYFILE"
|
||||
}
|
||||
|
||||
@test "the caddy service uses the locally built porkbun image" {
|
||||
grep -q 'franksdank/caddy-porkbun' "$REPO_ROOT/dankclaw/compose.yaml"
|
||||
@test "the caddy quadlet uses the locally built porkbun image without pulling" {
|
||||
# The image is built on-host from dankclaw/caddy/Containerfile; a boot
|
||||
# must not stall on a registry that does not have it.
|
||||
grep -q '^Image=franksdank/caddy-porkbun:latest$' \
|
||||
"$REPO_ROOT/dankclaw/caddy/quadlet/caddy.container"
|
||||
grep -q '^Pull=never$' "$REPO_ROOT/dankclaw/caddy/quadlet/caddy.container"
|
||||
}
|
||||
|
||||
@test "the caddy service passes the Porkbun credentials from .env" {
|
||||
grep -q 'PORKBUN_API_KEY: \${PORKBUN_API_KEY}' "$REPO_ROOT/dankclaw/compose.yaml"
|
||||
grep -q 'PORKBUN_API_SECRET_KEY: \${PORKBUN_API_SECRET_KEY}' "$REPO_ROOT/dankclaw/compose.yaml"
|
||||
@test "the caddy quadlet reads the Porkbun credentials from its env file" {
|
||||
grep -q '^EnvironmentFile=/home/dank/Containers/caddy/env$' \
|
||||
"$REPO_ROOT/dankclaw/caddy/quadlet/caddy.container"
|
||||
grep -q '^PORKBUN_API_KEY=changeme$' "$REPO_ROOT/dankclaw/caddy/env.example"
|
||||
grep -q '^PORKBUN_API_SECRET_KEY=changeme$' "$REPO_ROOT/dankclaw/caddy/env.example"
|
||||
}
|
||||
|
||||
@test ".env.example carries the Porkbun secret under the module's name" {
|
||||
|
||||
@@ -54,8 +54,13 @@ load helpers
|
||||
! grep -q 'deploy_gitea_unit gitea-caddy' "$REPO_ROOT/scripts/deploy-remote.sh"
|
||||
}
|
||||
|
||||
@test "canonical Caddy deployment recreates the container when needed" {
|
||||
grep -q 'podman-compose -f dankclaw/compose.yaml up -d --no-deps caddy' "$REPO_ROOT/scripts/deploy-remote.sh"
|
||||
@test "canonical Caddy deployment installs its boot-persistent quadlet" {
|
||||
# 2026-08-26: a bare compose container never came back after reboot.
|
||||
# deploy_caddy must install, enable, and start the quadlet unit instead
|
||||
# of `podman-compose up`.
|
||||
grep -q 'install_quadlet caddy dankclaw/caddy/quadlet caddy' "$REPO_ROOT/scripts/deploy-remote.sh"
|
||||
grep -q 'systemctl --user enable caddy.service' "$REPO_ROOT/scripts/deploy-remote.sh"
|
||||
! grep -q 'podman-compose -f dankclaw/compose.yaml up -d --no-deps caddy' "$REPO_ROOT/scripts/deploy-remote.sh"
|
||||
! grep -q 'no proxy was stopped or replaced' "$REPO_ROOT/scripts/deploy-remote.sh"
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,20 @@ HOSTS="$PIHOLE_DIR/addn-hosts"
|
||||
grep -qE '^Volume=/home/dank/Containers/pihole/addn-hosts:/etc/pihole/hosts/addn-hosts:ro$' "$QUADLET"
|
||||
}
|
||||
|
||||
@test "the pihole sysctl drop-in pins the unprivileged port threshold to 53" {
|
||||
# Rootless port 53 needs ip_unprivileged_port_start <= 53 at boot. The
|
||||
# repo drop-in is the single source of truth; a later-sorted file that
|
||||
# reset it to 80 took LAN DNS down after the 2026-08-26 reboot.
|
||||
local f="$PIHOLE_DIR/sysctl/99-dnsmasq.conf"
|
||||
[ -f "$f" ]
|
||||
grep -q '^net.ipv4.ip_unprivileged_port_start=53$' "$f"
|
||||
}
|
||||
|
||||
@test "deploy_pihole installs the sysctl drop-in and applies it at boot-path" {
|
||||
grep -q 'dankclaw/pihole/sysctl/99-dnsmasq.conf' "$REPO_ROOT/scripts/deploy-remote.sh"
|
||||
grep -q 'sysctl --system' "$REPO_ROOT/scripts/deploy-remote.sh"
|
||||
}
|
||||
|
||||
@test "the pihole env example has placeholder values only" {
|
||||
[ -f "$ENV_EXAMPLE" ]
|
||||
grep -q '^WEBPASSWORD=changeme$' "$ENV_EXAMPLE"
|
||||
|
||||
Reference in new issue
Block a user