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.
This commit is contained in:
Frank
2026-09-01 14:40:10 -07:00
parent b6be6d3da6
commit f4c9b4f381
9 changed files with 120 additions and 3 deletions
+12
View File
@@ -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
+1
View File
@@ -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
+43
View File
@@ -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
+3 -1
View File
@@ -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":
+7 -1
View File
@@ -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 ;;
+1 -1
View File
@@ -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|\
+7
View File
@@ -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.
;;
+1
View File
@@ -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},
+45
View File
@@ -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"
}