diff --git a/dankclaw/authentik/quadlet/authentik-outpost.container b/dankclaw/authentik/quadlet/authentik-outpost.container index ddd99cd..20c242e 100644 --- a/dankclaw/authentik/quadlet/authentik-outpost.container +++ b/dankclaw/authentik/quadlet/authentik-outpost.container @@ -20,7 +20,7 @@ Wants=authentik-server.service [Container] ContainerName=authentik-outpost -Image=ghcr.io/goauthentik/server:2026.8.3 +Image=ghcr.io/goauthentik/server:2026.8.3@sha256:ab9b4e8cc4ab3f8d1198d2db6aeea66bafea1963b3f2843589e0d163f97d9849 Exec=worker Network=dankclaw_default EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env diff --git a/dankclaw/authentik/quadlet/authentik-postgres.container b/dankclaw/authentik/quadlet/authentik-postgres.container index 8c623ba..53c43d0 100644 --- a/dankclaw/authentik/quadlet/authentik-postgres.container +++ b/dankclaw/authentik/quadlet/authentik-postgres.container @@ -14,7 +14,7 @@ After=network.target [Container] ContainerName=authentik-postgres -Image=docker.io/library/postgres:16-alpine +Image=docker.io/library/postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea Network=dankclaw_default EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env Environment=POSTGRES_DB=authentik diff --git a/dankclaw/authentik/quadlet/authentik-server.container b/dankclaw/authentik/quadlet/authentik-server.container index 156bcf7..d71c234 100644 --- a/dankclaw/authentik/quadlet/authentik-server.container +++ b/dankclaw/authentik/quadlet/authentik-server.container @@ -19,7 +19,7 @@ Wants=authentik-postgres.service [Container] ContainerName=authentik-server -Image=ghcr.io/goauthentik/server:2026.8.3 +Image=ghcr.io/goauthentik/server:2026.8.3@sha256:ab9b4e8cc4ab3f8d1198d2db6aeea66bafea1963b3f2843589e0d163f97d9849 Exec=server Network=dankclaw_default EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env diff --git a/dankclaw/authentik/quadlet/authentik-worker.container b/dankclaw/authentik/quadlet/authentik-worker.container index 2e4b8ed..81a572f 100644 --- a/dankclaw/authentik/quadlet/authentik-worker.container +++ b/dankclaw/authentik/quadlet/authentik-worker.container @@ -16,7 +16,7 @@ Wants=authentik-server.service [Container] ContainerName=authentik-worker -Image=ghcr.io/goauthentik/server:2026.8.3 +Image=ghcr.io/goauthentik/server:2026.8.3@sha256:ab9b4e8cc4ab3f8d1198d2db6aeea66bafea1963b3f2843589e0d163f97d9849 Exec=worker Network=dankclaw_default EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env diff --git a/dankclaw/gitea/quadlet/gitea-postgres.container b/dankclaw/gitea/quadlet/gitea-postgres.container index 29c8c70..663c162 100644 --- a/dankclaw/gitea/quadlet/gitea-postgres.container +++ b/dankclaw/gitea/quadlet/gitea-postgres.container @@ -15,7 +15,7 @@ After=network.target [Container] ContainerName=gitea-postgres Network=gitea.network -Image=docker.io/library/postgres:16-alpine +Image=docker.io/library/postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea EnvironmentFile=/home/dank/Containers/gitea/env Environment=POSTGRES_USER=gitea Environment=POSTGRES_PASSWORD=${GITEA_DB_PASSWORD} diff --git a/dankclaw/gitea/quadlet/gitea-server.container b/dankclaw/gitea/quadlet/gitea-server.container index 0179ff0..aae6efa 100644 --- a/dankclaw/gitea/quadlet/gitea-server.container +++ b/dankclaw/gitea/quadlet/gitea-server.container @@ -19,7 +19,7 @@ After=gitea-postgres.service network.target [Container] ContainerName=gitea-server Network=gitea.network -Image=docker.io/gitea/gitea:latest +Image=docker.io/gitea/gitea:latest@sha256:30777c91280d973fa02be48b1ade735bd6859e8f6f7af3b4444d519437852504 EnvironmentFile=/home/dank/Containers/gitea/env Environment=DATABASE_TYPE=postgres Environment=DATABASE_HOST=gitea-postgres diff --git a/dankclaw/marreta/quadlet/marreta-browser.container b/dankclaw/marreta/quadlet/marreta-browser.container index 9b6bd6e..f2f40dc 100644 --- a/dankclaw/marreta/quadlet/marreta-browser.container +++ b/dankclaw/marreta/quadlet/marreta-browser.container @@ -23,7 +23,7 @@ After=gluetun.service network.target [Container] ContainerName=marreta_browser Network=gluetun.container -Image=docker.io/lightpanda/browser:nightly +Image=docker.io/lightpanda/browser:nightly@sha256:979c36ed869f1016b8dcf22939a0633c262f2c30e892a03a9f720eba2ff1931e [Service] TimeoutStartSec=300 diff --git a/dankclaw/marreta/quadlet/marreta.container b/dankclaw/marreta/quadlet/marreta.container index b8780eb..81eb7fc 100644 --- a/dankclaw/marreta/quadlet/marreta.container +++ b/dankclaw/marreta/quadlet/marreta.container @@ -32,7 +32,7 @@ After=gluetun.service marreta-browser.service network.target [Container] ContainerName=marreta Network=gluetun.container -Image=ghcr.io/manualdousuario/marreta:latest +Image=ghcr.io/manualdousuario/marreta:latest@sha256:30f2a34567b79b66da794b4b9423751bbbcd07ceff600d9f6a2d219e7e439362 # Relocate off SearXNG's 8080 inside the shared netns. Environment=NGINX_HTTP_PORT=8085 Environment=APP_NAME=Marreta diff --git a/dankclaw/observability/quadlet/alertmanager.container b/dankclaw/observability/quadlet/alertmanager.container index 264221d..a21ebb3 100644 --- a/dankclaw/observability/quadlet/alertmanager.container +++ b/dankclaw/observability/quadlet/alertmanager.container @@ -33,7 +33,7 @@ Wants=network-online.target [Container] ContainerName=alertmanager -Image=docker.io/prom/alertmanager:latest +Image=docker.io/prom/alertmanager:latest@sha256:e9733bafb1bdef9b00e25a21f8f99dc26a22224bf16641ad754d1649f4c3357a Network=dankclaw_default User=0:0 Exec=--config.file=/etc/alertmanager/alertmanager.yml --storage.path=/alertmanager diff --git a/dankclaw/observability/quadlet/cadvisor.container b/dankclaw/observability/quadlet/cadvisor.container index 27b73b1..2b47ab1 100644 --- a/dankclaw/observability/quadlet/cadvisor.container +++ b/dankclaw/observability/quadlet/cadvisor.container @@ -35,7 +35,7 @@ Wants=network-online.target [Container] ContainerName=cadvisor -Image=gcr.io/cadvisor/cadvisor:v0.49.1 +Image=gcr.io/cadvisor/cadvisor:v0.49.1@sha256:3cde6faf0791ebf7b41d6f8ae7145466fed712ea6f252c935294d2608b1af388 PodmanArgs=--pid=host --cgroupns=host PublishPort=127.0.0.1:8080:8080 PublishPort=192.168.0.143:8080:8080 diff --git a/dankclaw/observability/quadlet/grafana.container b/dankclaw/observability/quadlet/grafana.container index 112b724..17770ec 100644 --- a/dankclaw/observability/quadlet/grafana.container +++ b/dankclaw/observability/quadlet/grafana.container @@ -34,7 +34,7 @@ Wants=network-online.target prometheus.service [Container] ContainerName=grafana -Image=docker.io/grafana/grafana:latest +Image=docker.io/grafana/grafana:latest@sha256:dbbc38ea21103cefb50396340cd88afec779c7e070b0a7bf5a75b4ce46651d3f Network=dankclaw_default User=0:0 PublishPort=127.0.0.1:3002:3000 diff --git a/dankclaw/observability/quadlet/node-exporter.container b/dankclaw/observability/quadlet/node-exporter.container index c4cb04c..4244c84 100644 --- a/dankclaw/observability/quadlet/node-exporter.container +++ b/dankclaw/observability/quadlet/node-exporter.container @@ -33,7 +33,7 @@ Wants=network-online.target [Container] ContainerName=node-exporter -Image=docker.io/prom/node-exporter:latest +Image=docker.io/prom/node-exporter:latest@sha256:1b4e4438faca4dd7e001dd445d161a4a2091b0fededa84093b3a8dfeae1f1be0 Network=host PodmanArgs=--pid=host Exec=--path.procfs=/host/proc --path.sysfs=/host/sys --path.rootfs=/rootfs --web.listen-address=127.0.0.1:9100 --web.listen-address=192.168.0.143:9100 diff --git a/dankclaw/observability/quadlet/prometheus.container b/dankclaw/observability/quadlet/prometheus.container index bbbb125..7bebfc4 100644 --- a/dankclaw/observability/quadlet/prometheus.container +++ b/dankclaw/observability/quadlet/prometheus.container @@ -43,14 +43,16 @@ Wants=network-online.target [Container] ContainerName=prometheus -Image=docker.io/prom/prometheus:latest +Image=docker.io/prom/prometheus:latest@sha256:fa3302279710be2f21a8301ec17a8b4f2301fd829bd113839b3b6b02b6f191a2 Network=dankclaw_default -# Name the podman gateway so scrape jobs can reach same-host targets (the -# netavark hairpin gap bars this container from dialing its own host's LAN -# IP; the cadvisor/gpu jobs and the blackbox scrape all ride this name). -# Same mechanism the hermes Caddy upstream learned the hard way (Container -# Host Reachability in the vault). -HostAddr=host.containers.internal=169.254.1.2 +# Same-host scrape targets are dialed at host.containers.internal (the podman +# gateway): a bridge container cannot dial its own host's LAN IP (netavark +# hairpin gap, measured 2026-10-21), and traffic to the gateway lands on host +# loopback / published ports (cadvisor/gpu jobs and the blackbox scrape all +# ride it). The name resolves natively — NO HostAddr key: quadlet 5.7.0 calls +# it unsupported and the WHOLE user quadlet batch then fails to generate, +# silently orphaning every other unit (live 2026-10-21: prometheus.service +# vanished while its container kept running as an orphan). User=0:0 Exec=--config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus --storage.tsdb.retention.time=30d --web.enable-lifecycle PublishPort=127.0.0.1:9090:9090 diff --git a/dankclaw/open-webui/quadlet/open-webui.container b/dankclaw/open-webui/quadlet/open-webui.container index 40bf3e1..51105e1 100644 --- a/dankclaw/open-webui/quadlet/open-webui.container +++ b/dankclaw/open-webui/quadlet/open-webui.container @@ -27,7 +27,7 @@ After=network.target [Container] ContainerName=open-webui Network=open-webui.network -Image=ghcr.io/open-webui/open-webui:main +Image=ghcr.io/open-webui/open-webui:main@sha256:4860ba005ae8f19f08d1e12b0868855b7b339ea0dad9927f196820fc0de04575 PublishPort=3005:8080 Environment=OPENAI_API_BASE_URL=http://192.168.0.143:7770 Environment=ANONYMIZED_TELEMETRY=False diff --git a/dankclaw/pihole/quadlet/pihole.container b/dankclaw/pihole/quadlet/pihole.container index b6aebc4..73d7d91 100644 --- a/dankclaw/pihole/quadlet/pihole.container +++ b/dankclaw/pihole/quadlet/pihole.container @@ -24,7 +24,7 @@ Wants=network-online.target [Container] ContainerName=pihole -Image=docker.io/pihole/pihole:latest +Image=docker.io/pihole/pihole:latest@sha256:d34134716fc32038b052a8674c7c6879e0ff21f0e6d8af345166fffa2c27c113 Network=slirp4netns PublishPort=0.0.0.0:53:53/udp PublishPort=0.0.0.0:53:53/tcp diff --git a/dankclaw/searxng/quadlet/gluetun.container b/dankclaw/searxng/quadlet/gluetun.container index 8fd81d7..a226e08 100644 --- a/dankclaw/searxng/quadlet/gluetun.container +++ b/dankclaw/searxng/quadlet/gluetun.container @@ -16,7 +16,7 @@ After=searxng-network.service network.target [Container] ContainerName=gluetun Network=searxng.network -Image=docker.io/qmcgaw/gluetun:latest +Image=docker.io/qmcgaw/gluetun:latest@sha256:c2ee4191762fca6b6a3c3359a731198a95d9c9af4db32d466e223bf3a84e779b AddCapability=NET_ADMIN AddDevice=/dev/net/tun:/dev/net/tun Environment=VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER} diff --git a/dankclaw/searxng/quadlet/searxng-valkey.container b/dankclaw/searxng/quadlet/searxng-valkey.container index 49d9f9f..9a58f0e 100644 --- a/dankclaw/searxng/quadlet/searxng-valkey.container +++ b/dankclaw/searxng/quadlet/searxng-valkey.container @@ -16,7 +16,7 @@ After=searxng-network.service network.target ContainerName=searxng-valkey Network=searxng.network NetworkAlias=valkey -Image=docker.io/valkey/valkey:9-alpine +Image=docker.io/valkey/valkey:9-alpine@sha256:48332870af354a799964c0012ae1194a0bf2bf894eb508f945810596dc2d8d11 Volume=/opt/searxng/valkey:/data [Service] diff --git a/dankclaw/searxng/quadlet/searxng.container b/dankclaw/searxng/quadlet/searxng.container index ae9c300..7961486 100644 --- a/dankclaw/searxng/quadlet/searxng.container +++ b/dankclaw/searxng/quadlet/searxng.container @@ -16,7 +16,7 @@ After=gluetun.service searxng-valkey.service network.target [Container] ContainerName=searxng Network=gluetun.container -Image=ghcr.io/searxng/searxng:latest +Image=ghcr.io/searxng/searxng:latest@sha256:5ed0323b5f5f8701264ef65e239a2bd83cb57098f2b971fa30bf79c06eeae271 Environment=SEARXNG_SECRET=${SEARXNG_SECRET} Environment=SEARXNG_BASE_URL=${SEARXNG_BASE_URL} Volume=/opt/searxng/config:/etc/searxng diff --git a/dankclaw/seerr/quadlet/seerr.container b/dankclaw/seerr/quadlet/seerr.container index 50e480e..f2adf47 100644 --- a/dankclaw/seerr/quadlet/seerr.container +++ b/dankclaw/seerr/quadlet/seerr.container @@ -29,7 +29,7 @@ Wants=network-online.target [Container] ContainerName=seerr -Image=ghcr.io/seerr-team/seerr:v3.4.1 +Image=ghcr.io/seerr-team/seerr:v3.4.1@sha256:f4768de5f616248d723e05891f3345a1402123775d03bf0890dbfedc0831bda1 Network=dankclaw_default PublishPort=127.0.0.1:5055:5055 Volume=/home/dank/Containers/seerr/data:/app/config diff --git a/dankserver/calibre-server/quadlet/calibre-server.container b/dankserver/calibre-server/quadlet/calibre-server.container index 69e6d26..e9d3762 100644 --- a/dankserver/calibre-server/quadlet/calibre-server.container +++ b/dankserver/calibre-server/quadlet/calibre-server.container @@ -43,7 +43,7 @@ After=network.target [Container] ContainerName=calibre-server -Image=lscr.io/linuxserver/calibre:9.15.0 +Image=lscr.io/linuxserver/calibre:9.15.0@sha256:59e4b0e036e62f228f8bcfae1ade42f47047719885fe454ddbad38e4597b987b Network=pasta EnvironmentFile=/home/dank/Containers/calibre-server/env Environment=PUID=1000 diff --git a/dankserver/calibre-web/quadlet/calibre-web.container b/dankserver/calibre-web/quadlet/calibre-web.container index 25e76b9..75a3c46 100644 --- a/dankserver/calibre-web/quadlet/calibre-web.container +++ b/dankserver/calibre-web/quadlet/calibre-web.container @@ -16,7 +16,7 @@ After=network.target [Container] ContainerName=calibre-web -Image=lscr.io/linuxserver/calibre-web:0.6.27-ls402 +Image=lscr.io/linuxserver/calibre-web:0.6.27-ls402@sha256:802729966faed5e32d671cd4f92ba6196e9e754a6c0f740ce1ee9acd86b10e55 Network=pasta Environment=DOCKER_MODS=linuxserver/mods:universal-calibre Environment=LANG=en_US.UTF-8 diff --git a/dankserver/copyparty/quadlet/copyparty.container b/dankserver/copyparty/quadlet/copyparty.container index 79869c5..76a31b0 100644 --- a/dankserver/copyparty/quadlet/copyparty.container +++ b/dankserver/copyparty/quadlet/copyparty.container @@ -21,7 +21,13 @@ After=network.target [Container] ContainerName=copyparty -Image=ghcr.io/9001/copyparty-ac:latest +# Digest-pinned: upstream stopped publishing version tags after 1.19.15, so +# the pin rides :latest@digest (the tag in the reference is cosmetic; the +# digest is the pull). Pinned 2026-10-21 at the build whose label is +# 1.20.26 - closes GHSA-3c88-p299-7hvr (1.20.25) and GHSA-hg99-jqfp-rf76 +# (1.20.26); the previously running build was the rolling :latest at +# 1.20.24 (issue #73 on command-center). +Image=ghcr.io/9001/copyparty-ac:latest@sha256:7f77459160a4bf34fdca79f2965dff4f0c1d7069a3d9cdfb3632acb041dd2f6a Network=dankserver_appnet EnvironmentFile=/home/dank/Containers/copyparty/env Exec=-a ${COPAY_ACCOUNT}:${COPAY_PASSWORD} -v /w::r:ra,dank diff --git a/dankserver/jellyfin/quadlet/jellyfin.container b/dankserver/jellyfin/quadlet/jellyfin.container index 1dfae33..88bc693 100644 --- a/dankserver/jellyfin/quadlet/jellyfin.container +++ b/dankserver/jellyfin/quadlet/jellyfin.container @@ -16,7 +16,7 @@ After=network.target [Container] ContainerName=jellyfin -Image=ghcr.io/jellyfin/jellyfin:latest +Image=ghcr.io/jellyfin/jellyfin:latest@sha256:c3efa69a22f2691fc88d6bb035e87a8560e395a6047f062fb381c61c3185e8f5 Network=dankserver_appnet Environment=DEBIAN_FRONTEND=noninteractive Environment=DOTNET_SYSTEM_IO_DISABLEFILELOCKING=true diff --git a/dankserver/observability/quadlet/cadvisor.container b/dankserver/observability/quadlet/cadvisor.container index 9a9e440..86a2c1b 100644 --- a/dankserver/observability/quadlet/cadvisor.container +++ b/dankserver/observability/quadlet/cadvisor.container @@ -18,7 +18,7 @@ Wants=network-online.target [Container] ContainerName=cadvisor -Image=gcr.io/cadvisor/cadvisor:v0.49.1 +Image=gcr.io/cadvisor/cadvisor:v0.49.1@sha256:3cde6faf0791ebf7b41d6f8ae7145466fed712ea6f252c935294d2608b1af388 PodmanArgs=--pid=host --cgroupns=host PublishPort=127.0.0.1:8080:8080 PublishPort=192.168.0.235:8080:8080 diff --git a/dankserver/observability/quadlet/node-exporter.container b/dankserver/observability/quadlet/node-exporter.container index 97a5cbf..45b8d41 100644 --- a/dankserver/observability/quadlet/node-exporter.container +++ b/dankserver/observability/quadlet/node-exporter.container @@ -37,7 +37,7 @@ Wants=network-online.target [Container] ContainerName=node-exporter -Image=docker.io/prom/node-exporter:latest +Image=docker.io/prom/node-exporter:latest@sha256:1b4e4438faca4dd7e001dd445d161a4a2091b0fededa84093b3a8dfeae1f1be0 Network=host PodmanArgs=--pid=host Exec=--path.procfs=/host/proc --path.sysfs=/host/sys --path.rootfs=/rootfs --web.listen-address=127.0.0.1:9100 --web.listen-address=192.168.0.235:9100 diff --git a/dankserver/plex/quadlet/plex.container b/dankserver/plex/quadlet/plex.container index 3a3131a..1a72e56 100644 --- a/dankserver/plex/quadlet/plex.container +++ b/dankserver/plex/quadlet/plex.container @@ -4,7 +4,7 @@ After=network-online.target Wants=network-online.target [Container] -Image=lscr.io/linuxserver/plex:1.43.5.11029-6bf8af3f0-ls329 +Image=lscr.io/linuxserver/plex:1.43.5.11029-6bf8af3f0-ls329@sha256:8f655bad7dcbc5398a78f70a5af12600b4ed3424f901566ed1eaa6710e9e2334 ContainerName=plex Network=dankserver_appnet:alias=plex Environment=PUID=0 diff --git a/dankserver/romm/quadlet/romm-mariadb.container b/dankserver/romm/quadlet/romm-mariadb.container index 9a546ec..130ebf8 100644 --- a/dankserver/romm/quadlet/romm-mariadb.container +++ b/dankserver/romm/quadlet/romm-mariadb.container @@ -14,7 +14,7 @@ After=network.target [Container] ContainerName=romm-mariadb -Image=docker.io/library/mariadb:11 +Image=docker.io/library/mariadb:11@sha256:f16d7a344413e519dff518f27bdb221388f84b0d274c97c4fb5f2cb0dc1b34fa Network=dankserver_appnet:alias=mariadb EnvironmentFile=/home/dank/Containers/romm/env Environment=MARIADB_DATABASE=romm diff --git a/dankserver/romm/quadlet/romm-redis.container b/dankserver/romm/quadlet/romm-redis.container index 0a6db6f..e935b37 100644 --- a/dankserver/romm/quadlet/romm-redis.container +++ b/dankserver/romm/quadlet/romm-redis.container @@ -12,7 +12,7 @@ After=network.target [Container] ContainerName=romm-redis -Image=docker.io/library/redis:7-alpine +Image=docker.io/library/redis:7-alpine@sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499 Network=dankserver_appnet:alias=redis Volume=/home/dank/Containers/romm/redis:/data HealthCmd=redis-cli ping diff --git a/dankserver/romm/quadlet/romm.container b/dankserver/romm/quadlet/romm.container index 347340c..858fc31 100644 --- a/dankserver/romm/quadlet/romm.container +++ b/dankserver/romm/quadlet/romm.container @@ -18,7 +18,7 @@ After=romm-mariadb.service romm-redis.service network.target [Container] ContainerName=romm -Image=ghcr.io/rommapp/romm:latest +Image=ghcr.io/rommapp/romm:latest@sha256:03295f95a6290e752641f9553ac4198b0e5b71b879d57f7b2371b98f608feb54 Network=dankserver_appnet EnvironmentFile=/home/dank/Containers/romm/env Environment=DB_HOST=mariadb diff --git a/dankserver/tautulli/quadlet/tautulli.container b/dankserver/tautulli/quadlet/tautulli.container index 41348c1..8ab4ae3 100644 --- a/dankserver/tautulli/quadlet/tautulli.container +++ b/dankserver/tautulli/quadlet/tautulli.container @@ -4,7 +4,7 @@ After=network-online.target Wants=network-online.target [Container] -Image=lscr.io/linuxserver/tautulli:v2.18.1-ls245 +Image=lscr.io/linuxserver/tautulli:v2.18.1-ls245@sha256:c66d3afa8f7a701a48dd1b1f57b22eca0483d895d670fe11ecff783f6a9bbd73 ContainerName=tautulli Network=dankserver_appnet:alias=tautulli Environment=PUID=0 diff --git a/frankbook-pro/gluetun/quadlet/gluetun.container b/frankbook-pro/gluetun/quadlet/gluetun.container index c7934cd..ae08c71 100644 --- a/frankbook-pro/gluetun/quadlet/gluetun.container +++ b/frankbook-pro/gluetun/quadlet/gluetun.container @@ -19,7 +19,7 @@ Description=Gluetun VPN tunnel (FrankBook-Pro podman machine) [Container] ContainerName=gluetun -Image=docker.io/qmcgaw/gluetun:latest +Image=docker.io/qmcgaw/gluetun:latest@sha256:c2ee4191762fca6b6a3c3359a731198a95d9c9af4db32d466e223bf3a84e779b AddCapability=NET_ADMIN AddDevice=/dev/net/tun:/dev/net/tun Environment=VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER} diff --git a/frankbook-pro/lidarr/quadlet/lidarr.container b/frankbook-pro/lidarr/quadlet/lidarr.container index 1b6a2e4..4e5a04c 100644 --- a/frankbook-pro/lidarr/quadlet/lidarr.container +++ b/frankbook-pro/lidarr/quadlet/lidarr.container @@ -15,7 +15,7 @@ After=gluetun.service [Container] ContainerName=lidarr -Image=lscr.io/linuxserver/lidarr:latest +Image=lscr.io/linuxserver/lidarr:latest@sha256:d8549ff9ea29b4015c32257360652c1000c31ea471a2b3f0835ae77292faf35b Environment=PUID=0 Environment=PGID=0 Environment=TZ=America/Los_Angeles diff --git a/frankbook-pro/prowlarr/quadlet/prowlarr.container b/frankbook-pro/prowlarr/quadlet/prowlarr.container index dc5bf3a..20f818d 100644 --- a/frankbook-pro/prowlarr/quadlet/prowlarr.container +++ b/frankbook-pro/prowlarr/quadlet/prowlarr.container @@ -24,7 +24,7 @@ After=gluetun.service [Container] ContainerName=prowlarr -Image=lscr.io/linuxserver/prowlarr:latest +Image=lscr.io/linuxserver/prowlarr:latest@sha256:b3b00047ed20f6ce2b16780145cb6a0acb51c9e28ea6aa6f5dcd33c7034f1797 Environment=PUID=0 Environment=PGID=0 Environment=TZ=America/Los_Angeles diff --git a/frankbook-pro/qbittorrent/quadlet/qbittorrent.container b/frankbook-pro/qbittorrent/quadlet/qbittorrent.container index 7f1066a..89de459 100644 --- a/frankbook-pro/qbittorrent/quadlet/qbittorrent.container +++ b/frankbook-pro/qbittorrent/quadlet/qbittorrent.container @@ -29,7 +29,7 @@ Network=gluetun.container # Pinned to the last 4.x line: Readarr's retired qB client requires the # qB 4.x login response (HTTP 200, body "Ok."); qB 5.x answers 204 with an # empty body, which fails Readarr's auth check. All other clients accept 4.x. -Image=lscr.io/linuxserver/qbittorrent:4.6.7 +Image=lscr.io/linuxserver/qbittorrent:4.6.7@sha256:55f15d44396315551f87294a176efae733b16e283f38980308e46073950257c6 Environment=PUID=0 Environment=PGID=0 Environment=TZ=America/Los_Angeles diff --git a/frankbook-pro/radarr/quadlet/radarr.container b/frankbook-pro/radarr/quadlet/radarr.container index ca81b16..c94fbb4 100644 --- a/frankbook-pro/radarr/quadlet/radarr.container +++ b/frankbook-pro/radarr/quadlet/radarr.container @@ -15,7 +15,7 @@ After=gluetun.service [Container] ContainerName=radarr -Image=lscr.io/linuxserver/radarr:latest +Image=lscr.io/linuxserver/radarr:latest@sha256:fc0ae3c50f8128b97e84381903ec8d6845c3a569641cb7117c124376cccb57c1 Environment=PUID=0 Environment=PGID=0 Environment=TZ=America/Los_Angeles diff --git a/frankbook-pro/readarr/quadlet/readarr.container b/frankbook-pro/readarr/quadlet/readarr.container index 60cf6c4..005b7fe 100644 --- a/frankbook-pro/readarr/quadlet/readarr.container +++ b/frankbook-pro/readarr/quadlet/readarr.container @@ -19,7 +19,7 @@ After=gluetun.service [Container] ContainerName=readarr -Image=lscr.io/linuxserver/readarr:amd64-1.0.980 +Image=lscr.io/linuxserver/readarr:amd64-1.0.980@sha256:a95e82dbb0eb2bb43de4c150f1dab9dd821306c67ea0b418983a957e731f2b61 Environment=PUID=0 Environment=PGID=0 Environment=TZ=America/Los_Angeles diff --git a/frankbook-pro/sonarr/quadlet/sonarr.container b/frankbook-pro/sonarr/quadlet/sonarr.container index 6d4ffcd..c16fbdb 100644 --- a/frankbook-pro/sonarr/quadlet/sonarr.container +++ b/frankbook-pro/sonarr/quadlet/sonarr.container @@ -16,7 +16,7 @@ After=gluetun.service [Container] ContainerName=sonarr -Image=lscr.io/linuxserver/sonarr:latest +Image=lscr.io/linuxserver/sonarr:latest@sha256:889dcc58c6aea859033985894b519b2e6b1e55d92527ed123f9f52e42ae7c97b Environment=PUID=0 Environment=PGID=0 Environment=TZ=America/Los_Angeles diff --git a/serverAppConfigs-docs/Architecture/Quadlet Lifecycle.md b/serverAppConfigs-docs/Architecture/Quadlet Lifecycle.md index c447265..379fa1c 100644 --- a/serverAppConfigs-docs/Architecture/Quadlet Lifecycle.md +++ b/serverAppConfigs-docs/Architecture/Quadlet Lifecycle.md @@ -130,10 +130,19 @@ Quickest unblock: `podman compose up -d --force-recreate `. Durable fix ## 4. Podman 5.x quadlet v1 generator quirks (verified live 2026-08-28) -- **Hard-fail keys** — the whole unit silently fails to generate - (`systemctl --user cat ` shows "could not be found"): any - dash-prefixed `[Container]` option (`--shm-size=…` → `unsupported key … - in group 'Container'`); `Command=` is unsupported, use `Exec=`. +- **Hard-fail keys abort the WHOLE BATCH, not one unit** (podman 5.7.0, + live 2026-10-21): one unsupported key — e.g. `HostAddr=` — makes the + generator log `unsupported key … in group 'Container'` plus + `processing encountered some errors` and generate NOTHING for that + manager. The running containers keep going as orphans, so the failure + first appears on the next deploy as `Unit .service not found` for a + service that is visibly up. Other offenders: any dash-prefixed + `[Container]` option (`--shm-size=…`), `Command=` (use `Exec=`). + The seam `tests/quadlet_image_pins.bats` fails the build on any + generator-rejected key in any host's units. + Also: an image whose ENTRYPOINT is the binary takes FLAGS ONLY in + `Exec=` — repeating the binary makes it a positional arg + (`unexpected /bin/blackbox_exporter`, live 2026-10-21). - **HealthCmd space-splitting:** the v1 generator splits `HealthCmd` on spaces into the healthcheck argv; a quoted probe `/bin/sh -c "pg_isready …"` lands as argv token `"pg_isready` → @@ -148,6 +157,16 @@ Quickest unblock: `podman compose up -d --force-recreate `. Durable fix "unsupported key"`, `systemctl --user cat probe.service` (shows the generated ExecStart verbatim), then remove + reload. +## 4a. Image pins (fleet law, 2026-10-21) + +Every registry image in a quadlet is digest-pinned (`tag@sha256:…`); +locally built images (`localhost/*`, `franksdank/*`) are exempt — their +build is the pin. A rolling tag lets a rebuild move under a running +service with nobody approving it, and it makes a CVE audit ungradeable. +Gate: `tests/quadlet_image_pins.bats`. Where upstream publishes no version +tags (copyparty-ac stops at 1.19.15), pin `:latest@sha256:` and name the +version the config label carries in the comment beside it. + ## 5. Rootless Quadlet log retrieval Step 0 — establish ownership: `systemctl --user status .service` diff --git a/tests/frankbook_quadlet.bats b/tests/frankbook_quadlet.bats index 29e094d..e2e6456 100755 --- a/tests/frankbook_quadlet.bats +++ b/tests/frankbook_quadlet.bats @@ -77,6 +77,7 @@ FRANKBOOK_UNITS=( } @test "readarr keeps its pinned image" { - grep -q '^Image=lscr.io/linuxserver/readarr:amd64-1.0.980$' \ + # 2026-10-21 fleet law: version tag AND digest (tests/quadlet_image_pins.bats) + grep -qE '^Image=lscr\.io/linuxserver/readarr:amd64-1\.0\.980@sha256:[0-9a-f]{64}$' \ "$REPO_ROOT/frankbook-pro/readarr/quadlet/readarr.container" || return 1 } diff --git a/tests/open_webui_quadlet.bats b/tests/open_webui_quadlet.bats index d02bdf2..44f0322 100644 --- a/tests/open_webui_quadlet.bats +++ b/tests/open_webui_quadlet.bats @@ -15,8 +15,9 @@ QDIR="$REPO_ROOT/dankclaw/open-webui/quadlet" } @test "open-webui uses the CPU image, not :cuda (dankclaw is CPU-only)" { - grep -qE '^Image=ghcr\.io/open-webui/open-webui:main$' "$QDIR/open-webui.container" \ - || { echo "open-webui.container must use the :main (CPU) image"; return 1; } + # 2026-10-21 fleet law: tag AND digest (tests/quadlet_image_pins.bats) + grep -qE '^Image=ghcr\.io/open-webui/open-webui:main@sha256:[0-9a-f]{64}$' "$QDIR/open-webui.container" \ + || { echo "open-webui.container must use the :main (CPU) image, digest-pinned"; return 1; } if grep -qE '^Image=.*:cuda' "$QDIR/open-webui.container"; then echo "open-webui.container must not use the :cuda image on a CPU-only host" return 1 diff --git a/tests/quadlet_image_pins.bats b/tests/quadlet_image_pins.bats new file mode 100755 index 0000000..c7bbe69 --- /dev/null +++ b/tests/quadlet_image_pins.bats @@ -0,0 +1,47 @@ +#!/usr/bin/env bats +# Seam: fleet-wide quadlet laws learned the hard way. +# 1. Every registry image is digest-pinned. A rolling tag (":latest", +# ":9-alpine", ":nightly") lets a rebuild move under a running +# service at the next restart/reboot with nobody approving it, and it +# makes a CVE audit ungradeable (you cannot score a tag). Locally +# built images (localhost/*, franksdank/*) are exempt: their build +# IS the pin, and they never pull. +# 2. No unit carries a key the podman 5.7.0 quadlet generator rejects. +# The generator aborts the WHOLE batch on an unsupported key, so one +# bad file silently un-generates every other unit in that manager - +# live 2026-10-21: HostAddr= in prometheus.container orphaned +# prometheus.service while its container kept running, and the next +# deploy died on "Unit prometheus.service not found". + +load helpers + +@test "every registry Image= is digest-pinned across all three hosts" { + bad="" + while IFS= read -r unit; do + while IFS= read -r img; do + case "$img" in + localhost/*|franksdank/*) continue ;; + esac + case "$img" in + *@sha256:*) ;; + *) bad="$bad$unit: $img"$'\n' ;; + esac + done < <(sed -n 's/^Image=//p' "$unit") + done < <(find "$REPO_ROOT"/dankserver "$REPO_ROOT"/dankclaw "$REPO_ROOT"/frankbook-pro \ + -path '*/quadlet/*.container' -not -path '*.AGENTTRASH*') + [ -z "$bad" ] || { printf 'unpinned images:\n%s' "$bad" >&2; return 1; } +} + +@test "no unit uses a key the 5.7.0 quadlet generator rejects (batch killer)" { + # HostAddr rejected by podman 5.7.0 (live 2026-10-21); Device= and + # Command= are the older v1 findings, guarded host-locally too. + # Keep this list current with every generator finding. + bad="" + while IFS= read -r unit; do + for key in HostAddr Device Command; do + grep -q "^${key}=" "$unit" && bad="$bad$unit: ${key}="$'\n' + done + done < <(find "$REPO_ROOT"/dankserver "$REPO_ROOT"/dankclaw "$REPO_ROOT"/frankbook-pro \ + -path '*/quadlet/*.container' -not -path '*.AGENTTRASH*') + [ -z "$bad" ] || { printf 'generator-rejected keys:\n%s' "$bad" >&2; return 1; } +} diff --git a/tests/seerr_quadlet.bats b/tests/seerr_quadlet.bats index 92f8be3..e7dbb1e 100755 --- a/tests/seerr_quadlet.bats +++ b/tests/seerr_quadlet.bats @@ -20,8 +20,9 @@ SEERR_UNIT="$REPO_ROOT/dankclaw/seerr/quadlet/seerr.container" 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 image is pinned to an exact version tag and digest" { + # 2026-10-21 fleet law: tag AND digest (tests/quadlet_image_pins.bats) + grep -Eq '^Image=ghcr\.io/seerr-team/seerr:v[0-9]+\.[0-9]+\.[0-9]+@sha256:[0-9a-f]{64}$' "$SEERR_UNIT" } @test "seerr publishes 5055 on loopback only (guard chain is the user path)" {