From f4c9b4f3814aaf5e89d7f50e601800c6293011c2 Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 1 Sep 2026 14:40:10 -0700 Subject: [PATCH] seerr: quadlet on dankclaw + guard-chain vhost + deployer wiring Unit: ghcr.io/seerr-team/seerr:v3.4.1, loopback-only 5055 publish (homepage probe; users go through Anubis->Authentik->caddy:8017->seerr), Containers data root, no env secrets. Guard chain: seerr.franksdank.cloud vhost behind anubis:8923 with post-auth handler on 8017; addn-hosts record; homepage widget; deploy-remote/deploy-plan/deploy.sh routing; seam test. --- dankclaw/Caddyfile | 12 +++++++ dankclaw/pihole/addn-hosts | 1 + dankclaw/seerr/quadlet/seerr.container | 43 ++++++++++++++++++++++++ scripts/deploy-plan.py | 4 ++- scripts/deploy-remote.sh | 8 ++++- scripts/deploy.sh | 2 +- scripts/test-changed.sh | 7 ++++ shared/homepage/services.json | 1 + tests/seerr_quadlet.bats | 45 ++++++++++++++++++++++++++ 9 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 dankclaw/seerr/quadlet/seerr.container create mode 100755 tests/seerr_quadlet.bats diff --git a/dankclaw/Caddyfile b/dankclaw/Caddyfile index bdf880d..62e6596 100644 --- a/dankclaw/Caddyfile +++ b/dankclaw/Caddyfile @@ -145,6 +145,13 @@ loki.franksdank.cloud { } } +# --- Seerr (guarded: Anubis → Authentik; seerr provider, internal_host caddy:8017) --- +seerr.franksdank.cloud { + reverse_proxy anubis:8923 { + header_up X-Real-Ip {remote_host} + } +} + # --- Pi-hole (quadlet on slirp4netns; published 8083) --- pihole.franksdank.cloud { basicauth { @@ -260,6 +267,11 @@ http://:8017 { reverse_proxy loki:3100 } + @seerr host seerr.franksdank.cloud + handle @seerr { + reverse_proxy seerr:5055 + } + @grafana host grafana.franksdank.cloud handle @grafana { reverse_proxy grafana:3000 diff --git a/dankclaw/pihole/addn-hosts b/dankclaw/pihole/addn-hosts index 965206c..59753ea 100644 --- a/dankclaw/pihole/addn-hosts +++ b/dankclaw/pihole/addn-hosts @@ -43,3 +43,4 @@ 192.168.0.143 prometheus.franksdank.cloud 192.168.0.143 loki.franksdank.cloud 192.168.0.143 pihole.franksdank.cloud +192.168.0.143 seerr.franksdank.cloud diff --git a/dankclaw/seerr/quadlet/seerr.container b/dankclaw/seerr/quadlet/seerr.container new file mode 100644 index 0000000..50e480e --- /dev/null +++ b/dankclaw/seerr/quadlet/seerr.container @@ -0,0 +1,43 @@ +# Quadlet unit: Seerr media request manager (rootless, boot-persistent). +# +# Install path on dankclaw: ~/.config/containers/systemd/seerr.container +# Unit name: seerr.service (container name: seerr) +# +# Seerr is the unified Overseerr/Jellyseerr successor. It fronts the +# *arr stack now living on FrankBook-Pro (192.168.0.112, resolved via the +# Pi-hole addn-hosts record frankbook-pro.lan): requests are placed here, +# routed through the arrs over the LAN, and downloads run inside the Mac's +# gluetun tunnel. SQLite lives under /home/dank/Containers/seerr/data — +# no separate database unit (decision D8). +# +# Exposure contract: the ONLY published port is loopback (127.0.0.1:5055) +# for the homepage probe and host-local checks. User traffic must traverse +# the guard chain: Caddy TLS edge → Anubis :8923 → Authentik outpost → +# Caddy :8017 post-auth listener → seerr:5055 on dankclaw_default. Never +# publish 5055 on a LAN or public address. +# +# Image pin: ghcr.io/seerr-team/seerr:v3.4.1 (manifest-verified on GHCR +# 2026-09-01; released 2026-07-30). Upgrade = bump the tag, redeploy. +# +# No secrets in this file: OIDC (Authentik) and arr API keys live in +# Seerr's own settings store inside the data volume, configured at runtime. + +[Unit] +Description=Seerr media request manager container +After=network-online.target +Wants=network-online.target + +[Container] +ContainerName=seerr +Image=ghcr.io/seerr-team/seerr:v3.4.1 +Network=dankclaw_default +PublishPort=127.0.0.1:5055:5055 +Volume=/home/dank/Containers/seerr/data:/app/config + +[Service] +Restart=always +RestartSec=5 +TimeoutStartSec=900 + +[Install] +WantedBy=default.target diff --git a/scripts/deploy-plan.py b/scripts/deploy-plan.py index ce5341b..49ebb46 100755 --- a/scripts/deploy-plan.py +++ b/scripts/deploy-plan.py @@ -28,6 +28,7 @@ SERVICE_ORDER = ( "gitea-postgres", "gitea-server", "comfyui", + "seerr", "hermes", "hermes-dashboard", "hermes-webui", @@ -70,6 +71,7 @@ DANKCLAW_SERVICES = { "gitea-postgres", "gitea-server", "comfyui", + "seerr", "hermes", "hermes-dashboard", "prometheus", @@ -269,7 +271,7 @@ def services_for_path(path: str, target: str) -> set[str]: if len(parts) >= 2 and parts[1] == "Containerfile.comfyui": _add(services, "comfyui") return services - if len(parts) >= 2 and parts[1] in {"caddy", "comfyui", "loki", "alloy", "hermes", "anubis"}: + if len(parts) >= 2 and parts[1] in {"caddy", "comfyui", "loki", "alloy", "hermes", "anubis", "seerr"}: _add(services, parts[1]) return services if len(parts) >= 4 and parts[1] == "authentik" and parts[2] == "quadlet": diff --git a/scripts/deploy-remote.sh b/scripts/deploy-remote.sh index 608ee40..a1111fc 100755 --- a/scripts/deploy-remote.sh +++ b/scripts/deploy-remote.sh @@ -89,7 +89,7 @@ service_is_allowed() { dankclaw:loki|dankclaw:alloy|dankclaw:node-exporter|\ dankclaw:authentik-postgres|dankclaw:authentik-server|\ dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:anubis|\ - dankclaw:caddy|dankclaw:homepage|\ + dankclaw:caddy|dankclaw:homepage|dankclaw:seerr|\ dankclaw:frank-default-large|dankclaw:frank-default-npu|\ dankclaw:frank-default-sightsound|\ dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\ @@ -952,6 +952,11 @@ VM_START echo " $service answers on 127.0.0.1:$port" } +deploy_seerr() { + install_quadlet seerr dankclaw/seerr/quadlet seerr + systemctl --user restart seerr.service +} + deploy_service() { local service="$1" if [[ "$TARGET" == "frankbook-pro" ]]; then @@ -977,6 +982,7 @@ deploy_service() { deploy_authentik_outpost ;; anubis) deploy_anubis ;; + seerr) deploy_seerr ;; frank-default-large) deploy_llm_large ;; frank-default-small) deploy_llm_small ;; frank-default-npu) deploy_llm_npu ;; diff --git a/scripts/deploy.sh b/scripts/deploy.sh index b6ac733..9f58eaa 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -133,7 +133,7 @@ valid_service() { dankclaw:comfyui|dankclaw:hermes|dankclaw:hermes-dashboard|\ dankclaw:hermes-webui|dankclaw:prometheus|dankclaw:grafana|dankclaw:loki|dankclaw:alloy|\ dankclaw:node-exporter|dankclaw:authentik-postgres|dankclaw:authentik-server|\ - dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:anubis|\ + dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:anubis|dankclaw:seerr|\ dankclaw:frank-default-large|dankclaw:frank-default-npu|\ dankclaw:frank-default-sightsound|\ dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\ diff --git a/scripts/test-changed.sh b/scripts/test-changed.sh index 99e3968..702e231 100755 --- a/scripts/test-changed.sh +++ b/scripts/test-changed.sh @@ -221,6 +221,13 @@ for path in "${CHANGED[@]}"; do add_test tests/deployment_flow.bats add_test tests/deployment_scoping.bats ;; + # Seerr unit on dankclaw: boot persistence, loopback-only port + # posture, and the deployer routing that installs it. + dankclaw/seerr/*) + add_test tests/seerr_quadlet.bats + add_test tests/deployment_flow.bats + add_test tests/deployment_scoping.bats + ;; report/*) # Frozen audit reports are point-in-time artifacts; not testable. ;; diff --git a/shared/homepage/services.json b/shared/homepage/services.json index c31f413..24293e0 100644 --- a/shared/homepage/services.json +++ b/shared/homepage/services.json @@ -8,6 +8,7 @@ {"name":"Copyparty","category":"user","description":"File sharing over the LAN.","url":"http://files.franksdank.cloud","docs":"","probe":"192.168.0.235:3923","method":"http","wip":false,"showLink":true}, {"name":"Plex","category":"user","description":"Media server.","url":"http://plex.franksdank.cloud","docs":"","probe":"192.168.0.235:32400","method":"http","wip":false,"showLink":true}, {"name":"Jellyfin","category":"user","description":"Media server.","url":"http://jellyfin.franksdank.cloud","docs":"","probe":"192.168.0.235:8096","method":"http","wip":false,"showLink":true}, + {"name":"Seerr","category":"user","description":"Requests for the *arr stack.","url":"http://seerr.franksdank.cloud","docs":"","probe":"127.0.0.1:5055","method":"http","wip":false,"showLink":true}, {"name":"Hermes","category":"admin","description":"The agent gateway.","url":"http://hermes.franksdank.cloud","docs":"","probe":"127.0.0.1:8090","method":"http","wip":false,"showLink":true}, {"name":"Grafana","category":"admin","description":"Dashboards.","url":"http://grafana.franksdank.cloud","docs":"","probe":"127.0.0.1:3002","method":"http","wip":false,"showLink":true}, diff --git a/tests/seerr_quadlet.bats b/tests/seerr_quadlet.bats new file mode 100755 index 0000000..92f8be3 --- /dev/null +++ b/tests/seerr_quadlet.bats @@ -0,0 +1,45 @@ +#!/usr/bin/env bats +# Seam: the Seerr quadlet on dankclaw. Pins boot persistence, self-healing, +# the pinned image tag, the guard chain's port posture (loopback-only +# publish — the guard chain is the only user path), the Containers data +# root, and zero env-carried secret material in the tracked unit. + +load helpers + +SEERR_UNIT="$REPO_ROOT/dankclaw/seerr/quadlet/seerr.container" + +@test "seerr quadlet exists under dankclaw/seerr/quadlet/" { + [ -f "$SEERR_UNIT" ] +} + +@test "seerr unit is boot-persistent (WantedBy=default.target)" { + grep -q '^WantedBy=default.target$' "$SEERR_UNIT" +} + +@test "seerr unit is self-healing (Restart=always)" { + grep -q '^Restart=always$' "$SEERR_UNIT" +} + +@test "seerr image is pinned to an exact version tag" { + grep -Eq '^Image=ghcr\.io/seerr-team/seerr:v[0-9]+\.[0-9]+\.[0-9]+$' "$SEERR_UNIT" +} + +@test "seerr publishes 5055 on loopback only (guard chain is the user path)" { + grep -q '^PublishPort=127.0.0.1:5055:5055$' "$SEERR_UNIT" + if grep -Eq '^PublishPort=' "$SEERR_UNIT"; then + grep -Eq '^PublishPort=127\.0\.0\.1:5055:5055$' "$SEERR_UNIT" + [ "$(grep -c '^PublishPort=' "$SEERR_UNIT")" -eq 1 ] + fi +} + +@test "seerr data lives under the Containers data root" { + grep -q '^Volume=/home/dank/Containers/seerr/data:/app/config$' "$SEERR_UNIT" +} + +@test "seerr joins the app network for the post-auth route" { + grep -q '^Network=dankclaw_default$' "$SEERR_UNIT" +} + +@test "seerr unit carries no env or env-file secret material" { + ! grep -Eq '^(Environment|EnvironmentFile)=' "$SEERR_UNIT" +}