Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2810ab610 | ||
|
|
7a8cc83385 | ||
|
|
c4623f1629 | ||
|
|
065a6911f9 | ||
|
|
745d66d60d | ||
|
|
4009f118c1 | ||
|
|
7de343c171 | ||
|
|
139a3cd4fa | ||
|
|
5ffbf19fb2 | ||
|
|
1fd98cab3f | ||
|
|
d61c19d41f |
No files matched your search
@@ -20,7 +20,7 @@ Wants=authentik-server.service
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=authentik-outpost
|
ContainerName=authentik-outpost
|
||||||
Image=ghcr.io/goauthentik/server:2026.8.3
|
Image=ghcr.io/goauthentik/server:2026.8.3@sha256:ab9b4e8cc4ab3f8d1198d2db6aeea66bafea1963b3f2843589e0d163f97d9849
|
||||||
Exec=worker
|
Exec=worker
|
||||||
Network=dankclaw_default
|
Network=dankclaw_default
|
||||||
EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env
|
EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ After=network.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=authentik-postgres
|
ContainerName=authentik-postgres
|
||||||
Image=docker.io/library/postgres:16-alpine
|
Image=docker.io/library/postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea
|
||||||
Network=dankclaw_default
|
Network=dankclaw_default
|
||||||
EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env
|
EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env
|
||||||
Environment=POSTGRES_DB=authentik
|
Environment=POSTGRES_DB=authentik
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Wants=authentik-postgres.service
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=authentik-server
|
ContainerName=authentik-server
|
||||||
Image=ghcr.io/goauthentik/server:2026.8.3
|
Image=ghcr.io/goauthentik/server:2026.8.3@sha256:ab9b4e8cc4ab3f8d1198d2db6aeea66bafea1963b3f2843589e0d163f97d9849
|
||||||
Exec=server
|
Exec=server
|
||||||
Network=dankclaw_default
|
Network=dankclaw_default
|
||||||
EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env
|
EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Wants=authentik-server.service
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=authentik-worker
|
ContainerName=authentik-worker
|
||||||
Image=ghcr.io/goauthentik/server:2026.8.3
|
Image=ghcr.io/goauthentik/server:2026.8.3@sha256:ab9b4e8cc4ab3f8d1198d2db6aeea66bafea1963b3f2843589e0d163f97d9849
|
||||||
Exec=worker
|
Exec=worker
|
||||||
Network=dankclaw_default
|
Network=dankclaw_default
|
||||||
EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env
|
EnvironmentFile=/home/dank/serverAppConfigs/dankclaw/.env
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ After=network.target
|
|||||||
[Container]
|
[Container]
|
||||||
ContainerName=gitea-postgres
|
ContainerName=gitea-postgres
|
||||||
Network=gitea.network
|
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
|
EnvironmentFile=/home/dank/Containers/gitea/env
|
||||||
Environment=POSTGRES_USER=gitea
|
Environment=POSTGRES_USER=gitea
|
||||||
Environment=POSTGRES_PASSWORD=${GITEA_DB_PASSWORD}
|
Environment=POSTGRES_PASSWORD=${GITEA_DB_PASSWORD}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ After=gitea-postgres.service network.target
|
|||||||
[Container]
|
[Container]
|
||||||
ContainerName=gitea-server
|
ContainerName=gitea-server
|
||||||
Network=gitea.network
|
Network=gitea.network
|
||||||
Image=docker.io/gitea/gitea:latest
|
Image=docker.io/gitea/gitea:latest@sha256:30777c91280d973fa02be48b1ade735bd6859e8f6f7af3b4444d519437852504
|
||||||
EnvironmentFile=/home/dank/Containers/gitea/env
|
EnvironmentFile=/home/dank/Containers/gitea/env
|
||||||
Environment=DATABASE_TYPE=postgres
|
Environment=DATABASE_TYPE=postgres
|
||||||
Environment=DATABASE_HOST=gitea-postgres
|
Environment=DATABASE_HOST=gitea-postgres
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ After=gluetun.service network.target
|
|||||||
[Container]
|
[Container]
|
||||||
ContainerName=marreta_browser
|
ContainerName=marreta_browser
|
||||||
Network=gluetun.container
|
Network=gluetun.container
|
||||||
Image=docker.io/lightpanda/browser:nightly
|
Image=docker.io/lightpanda/browser:nightly@sha256:979c36ed869f1016b8dcf22939a0633c262f2c30e892a03a9f720eba2ff1931e
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
TimeoutStartSec=300
|
TimeoutStartSec=300
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ After=gluetun.service marreta-browser.service network.target
|
|||||||
[Container]
|
[Container]
|
||||||
ContainerName=marreta
|
ContainerName=marreta
|
||||||
Network=gluetun.container
|
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.
|
# Relocate off SearXNG's 8080 inside the shared netns.
|
||||||
Environment=NGINX_HTTP_PORT=8085
|
Environment=NGINX_HTTP_PORT=8085
|
||||||
Environment=APP_NAME=Marreta
|
Environment=APP_NAME=Marreta
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=alertmanager
|
ContainerName=alertmanager
|
||||||
Image=docker.io/prom/alertmanager:latest
|
Image=docker.io/prom/alertmanager:latest@sha256:e9733bafb1bdef9b00e25a21f8f99dc26a22224bf16641ad754d1649f4c3357a
|
||||||
Network=dankclaw_default
|
Network=dankclaw_default
|
||||||
User=0:0
|
User=0:0
|
||||||
Exec=--config.file=/etc/alertmanager/alertmanager.yml --storage.path=/alertmanager
|
Exec=--config.file=/etc/alertmanager/alertmanager.yml --storage.path=/alertmanager
|
||||||
|
|||||||
@@ -1,29 +1,50 @@
|
|||||||
# Quadlet unit: blackbox exporter (rootless, boot-persistent).
|
# Quadlet unit: blackbox exporter (rootless, host network, boot-persistent).
|
||||||
#
|
#
|
||||||
# Install path on dankclaw:
|
# Install path on dankclaw:
|
||||||
# ~/.config/containers/systemd/blackbox-exporter.container
|
# ~/.config/containers/systemd/blackbox-exporter.container
|
||||||
#
|
#
|
||||||
# WHY Network=host. The probes it runs are (a) the host's own LAN IP
|
# WHY Network=host. The probes it runs are (a) the host's own LAN IP
|
||||||
# (192.168.0.143:<port>) and (b) container names on dankclaw_default (comfyui).
|
# (192.168.0.143:8081 SearXNG, :3005 OpenWebUI — host-process/portmap binds a
|
||||||
# A bridge container cannot reach the host's LAN IP at an unpublished port
|
# bridge container cannot reach; netavark DNATs published ports only, see
|
||||||
# (netavark DNATs published ports only — see Container Host Reachability in the
|
# Container Host Reachability in the vault) and (b) ComfyUI's published port
|
||||||
# vault); a host-netns process reaches both. Prometheus (bridge) reaches THIS
|
# via 127.0.0.1 (rootless --network=host shares the real host netns, so the
|
||||||
# container the same way it reaches the signal relay: the quadlet-published
|
# 0.0.0.0:8188 publish answers on loopback). A bridge-networked prober could
|
||||||
# host.containers.internal address (169.254.1.2), mapped via HostAddr= on the
|
# reach neither class reliably.
|
||||||
# prometheus unit.
|
|
||||||
#
|
#
|
||||||
# The 9115 metrics port publishes on loopback only — it is scraped by the
|
# WHY the listener binds 127.0.0.1 + LAN. A bridge container CANNOT dial
|
||||||
# prometheus container through the gateway, and never opened on the LAN.
|
# its own host's LAN IP (netavark hairpin gap, measured 2026-10-21), but a
|
||||||
|
# bridge container dialing the podman gateway 169.254.1.2 lands on the host
|
||||||
|
# LOOPBACK — that is the scrape path, and it needs this exporter to be
|
||||||
|
# LISTENING ON LOOPBACK (proved: 200 from the prometheus container to
|
||||||
|
# http://169.254.1.2:9115/-/healthy with a loopback bind). The gateway
|
||||||
|
# address itself is NOT bindable from the host netns — it exists only
|
||||||
|
# inside each rootless netns; binding it dies "bind: cannot assign
|
||||||
|
# requested address" (live 2026-10-21). The LAN bind stays for diagnostics.
|
||||||
|
# No public DNS name, no guard chain: internal net only, per the exposure
|
||||||
|
# policy. The listen flag is --web.listen-address and it is REPEATABLE, one
|
||||||
|
# flag per bind; plain --listen-address does not exist and the exporter
|
||||||
|
# exits in 100 ms with "unknown long flag" (live 2026-10-21).
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Blackbox exporter (HTTP probe prober)
|
Description=Blackbox exporter (HTTP probe prober)
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
|
Image=docker.io/prom/blackbox-exporter@sha256:ea1c7aa3ef3f307cd03e85f34abb604478ccb90b567fc690da0cd9776da76b13
|
||||||
ContainerName=blackbox-exporter
|
ContainerName=blackbox-exporter
|
||||||
Exec=/bin/blackbox_exporter --config.file=/etc/blackbox_exporter/config.yml
|
Network=host
|
||||||
|
# The image's ENTRYPOINT is /bin/blackbox_exporter itself (proved live
|
||||||
|
# 2026-10-21: `podman run IMAGE sh` dies "unexpected sh"), so Exec= carries
|
||||||
|
# FLAGS ONLY — repeating the binary makes it a positional arg and kingpin
|
||||||
|
# exits "unexpected /bin/blackbox_exporter".
|
||||||
|
Exec=--config.file=/etc/blackbox_exporter/config.yml --web.listen-address=127.0.0.1:9115 --web.listen-address=192.168.0.143:9115
|
||||||
Volume=/opt/blackbox/blackbox.yml:/etc/blackbox_exporter/config.yml:ro
|
Volume=/opt/blackbox/blackbox.yml:/etc/blackbox_exporter/config.yml:ro
|
||||||
HealthCmd=/bin/sh -c "wget -q -O /dev/null http://127.0.0.1:9115/-/healthy || exit 1"
|
# Plain exec form like every sibling unit. Shell form (`/bin/sh -c "…"`)
|
||||||
|
# is passed as one quoted token and quadlet then folds the following
|
||||||
|
# --health-interval/--health-retries (and the image name) into the command
|
||||||
|
# string — unit died twice that way, live 2026-10-21. wget only; this
|
||||||
|
# busybox variant has no curl, and host-network wget answers on loopback.
|
||||||
|
HealthCmd=wget -q -O /dev/null http://127.0.0.1:9115/-/healthy
|
||||||
HealthInterval=30s
|
HealthInterval=30s
|
||||||
HealthRetries=3
|
HealthRetries=3
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=cadvisor
|
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
|
PodmanArgs=--pid=host --cgroupns=host
|
||||||
PublishPort=127.0.0.1:8080:8080
|
PublishPort=127.0.0.1:8080:8080
|
||||||
PublishPort=192.168.0.143:8080:8080
|
PublishPort=192.168.0.143:8080:8080
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Wants=network-online.target prometheus.service
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=grafana
|
ContainerName=grafana
|
||||||
Image=docker.io/grafana/grafana:latest
|
Image=docker.io/grafana/grafana:latest@sha256:dbbc38ea21103cefb50396340cd88afec779c7e070b0a7bf5a75b4ce46651d3f
|
||||||
Network=dankclaw_default
|
Network=dankclaw_default
|
||||||
User=0:0
|
User=0:0
|
||||||
PublishPort=127.0.0.1:3002:3000
|
PublishPort=127.0.0.1:3002:3000
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=node-exporter
|
ContainerName=node-exporter
|
||||||
Image=docker.io/prom/node-exporter:latest
|
Image=docker.io/prom/node-exporter:latest@sha256:1b4e4438faca4dd7e001dd445d161a4a2091b0fededa84093b3a8dfeae1f1be0
|
||||||
Network=host
|
Network=host
|
||||||
PodmanArgs=--pid=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
|
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
|
||||||
|
|||||||
@@ -43,13 +43,16 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=prometheus
|
ContainerName=prometheus
|
||||||
Image=docker.io/prom/prometheus:latest
|
Image=docker.io/prom/prometheus:latest@sha256:fa3302279710be2f21a8301ec17a8b4f2301fd829bd113839b3b6b02b6f191a2
|
||||||
Network=dankclaw_default
|
Network=dankclaw_default
|
||||||
# Name the podman gateway so scrape jobs can reach host-netns processes (the
|
# Same-host scrape targets are dialed at host.containers.internal (the podman
|
||||||
# blackbox exporter listens on the host's loopback; a bridge container reaches
|
# gateway): a bridge container cannot dial its own host's LAN IP (netavark
|
||||||
# it at this address, never at 127.0.0.1). Same mechanism the hermes Caddy
|
# hairpin gap, measured 2026-10-21), and traffic to the gateway lands on host
|
||||||
# upstream learned the hard way (Container Host Reachability in the vault).
|
# loopback / published ports (cadvisor/gpu jobs and the blackbox scrape all
|
||||||
HostAddr=host.containers.internal=169.254.1.2
|
# 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
|
User=0:0
|
||||||
Exec=--config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus --storage.tsdb.retention.time=30d --web.enable-lifecycle
|
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
|
PublishPort=127.0.0.1:9090:9090
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ After=network.target
|
|||||||
[Container]
|
[Container]
|
||||||
ContainerName=open-webui
|
ContainerName=open-webui
|
||||||
Network=open-webui.network
|
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
|
PublishPort=3005:8080
|
||||||
Environment=OPENAI_API_BASE_URL=http://192.168.0.143:7770
|
Environment=OPENAI_API_BASE_URL=http://192.168.0.143:7770
|
||||||
Environment=ANONYMIZED_TELEMETRY=False
|
Environment=ANONYMIZED_TELEMETRY=False
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=pihole
|
ContainerName=pihole
|
||||||
Image=docker.io/pihole/pihole:latest
|
Image=docker.io/pihole/pihole:latest@sha256:d34134716fc32038b052a8674c7c6879e0ff21f0e6d8af345166fffa2c27c113
|
||||||
Network=slirp4netns
|
Network=slirp4netns
|
||||||
PublishPort=0.0.0.0:53:53/udp
|
PublishPort=0.0.0.0:53:53/udp
|
||||||
PublishPort=0.0.0.0:53:53/tcp
|
PublishPort=0.0.0.0:53:53/tcp
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ After=searxng-network.service network.target
|
|||||||
[Container]
|
[Container]
|
||||||
ContainerName=gluetun
|
ContainerName=gluetun
|
||||||
Network=searxng.network
|
Network=searxng.network
|
||||||
Image=docker.io/qmcgaw/gluetun:latest
|
Image=docker.io/qmcgaw/gluetun:latest@sha256:c2ee4191762fca6b6a3c3359a731198a95d9c9af4db32d466e223bf3a84e779b
|
||||||
AddCapability=NET_ADMIN
|
AddCapability=NET_ADMIN
|
||||||
AddDevice=/dev/net/tun:/dev/net/tun
|
AddDevice=/dev/net/tun:/dev/net/tun
|
||||||
Environment=VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
|
Environment=VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ After=searxng-network.service network.target
|
|||||||
ContainerName=searxng-valkey
|
ContainerName=searxng-valkey
|
||||||
Network=searxng.network
|
Network=searxng.network
|
||||||
NetworkAlias=valkey
|
NetworkAlias=valkey
|
||||||
Image=docker.io/valkey/valkey:9-alpine
|
Image=docker.io/valkey/valkey:9-alpine@sha256:48332870af354a799964c0012ae1194a0bf2bf894eb508f945810596dc2d8d11
|
||||||
Volume=/opt/searxng/valkey:/data
|
Volume=/opt/searxng/valkey:/data
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ After=gluetun.service searxng-valkey.service network.target
|
|||||||
[Container]
|
[Container]
|
||||||
ContainerName=searxng
|
ContainerName=searxng
|
||||||
Network=gluetun.container
|
Network=gluetun.container
|
||||||
Image=ghcr.io/searxng/searxng:latest
|
Image=ghcr.io/searxng/searxng:latest@sha256:5ed0323b5f5f8701264ef65e239a2bd83cb57098f2b971fa30bf79c06eeae271
|
||||||
Environment=SEARXNG_SECRET=${SEARXNG_SECRET}
|
Environment=SEARXNG_SECRET=${SEARXNG_SECRET}
|
||||||
Environment=SEARXNG_BASE_URL=${SEARXNG_BASE_URL}
|
Environment=SEARXNG_BASE_URL=${SEARXNG_BASE_URL}
|
||||||
Volume=/opt/searxng/config:/etc/searxng
|
Volume=/opt/searxng/config:/etc/searxng
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=seerr
|
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
|
Network=dankclaw_default
|
||||||
PublishPort=127.0.0.1:5055:5055
|
PublishPort=127.0.0.1:5055:5055
|
||||||
Volume=/home/dank/Containers/seerr/data:/app/config
|
Volume=/home/dank/Containers/seerr/data:/app/config
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ After=network.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=calibre-server
|
ContainerName=calibre-server
|
||||||
Image=lscr.io/linuxserver/calibre:9.15.0
|
Image=lscr.io/linuxserver/calibre:9.15.0@sha256:59e4b0e036e62f228f8bcfae1ade42f47047719885fe454ddbad38e4597b987b
|
||||||
Network=pasta
|
Network=pasta
|
||||||
EnvironmentFile=/home/dank/Containers/calibre-server/env
|
EnvironmentFile=/home/dank/Containers/calibre-server/env
|
||||||
Environment=PUID=1000
|
Environment=PUID=1000
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ After=network.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=calibre-web
|
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
|
Network=pasta
|
||||||
Environment=DOCKER_MODS=linuxserver/mods:universal-calibre
|
Environment=DOCKER_MODS=linuxserver/mods:universal-calibre
|
||||||
Environment=LANG=en_US.UTF-8
|
Environment=LANG=en_US.UTF-8
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ After=network.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=copyparty
|
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
|
Network=dankserver_appnet
|
||||||
EnvironmentFile=/home/dank/Containers/copyparty/env
|
EnvironmentFile=/home/dank/Containers/copyparty/env
|
||||||
Exec=-a ${COPAY_ACCOUNT}:${COPAY_PASSWORD} -v /w::r:ra,dank
|
Exec=-a ${COPAY_ACCOUNT}:${COPAY_PASSWORD} -v /w::r:ra,dank
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ After=network.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=jellyfin
|
ContainerName=jellyfin
|
||||||
Image=ghcr.io/jellyfin/jellyfin:latest
|
Image=ghcr.io/jellyfin/jellyfin:latest@sha256:c3efa69a22f2691fc88d6bb035e87a8560e395a6047f062fb381c61c3185e8f5
|
||||||
Network=dankserver_appnet
|
Network=dankserver_appnet
|
||||||
Environment=DEBIAN_FRONTEND=noninteractive
|
Environment=DEBIAN_FRONTEND=noninteractive
|
||||||
Environment=DOTNET_SYSTEM_IO_DISABLEFILELOCKING=true
|
Environment=DOTNET_SYSTEM_IO_DISABLEFILELOCKING=true
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=cadvisor
|
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
|
PodmanArgs=--pid=host --cgroupns=host
|
||||||
PublishPort=127.0.0.1:8080:8080
|
PublishPort=127.0.0.1:8080:8080
|
||||||
PublishPort=192.168.0.235:8080:8080
|
PublishPort=192.168.0.235:8080:8080
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=node-exporter
|
ContainerName=node-exporter
|
||||||
Image=docker.io/prom/node-exporter:latest
|
Image=docker.io/prom/node-exporter:latest@sha256:1b4e4438faca4dd7e001dd445d161a4a2091b0fededa84093b3a8dfeae1f1be0
|
||||||
Network=host
|
Network=host
|
||||||
PodmanArgs=--pid=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
|
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
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ After=network-online.target
|
|||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
[Container]
|
[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
|
ContainerName=plex
|
||||||
Network=dankserver_appnet:alias=plex
|
Network=dankserver_appnet:alias=plex
|
||||||
Environment=PUID=0
|
Environment=PUID=0
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ After=network.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=romm-mariadb
|
ContainerName=romm-mariadb
|
||||||
Image=docker.io/library/mariadb:11
|
Image=docker.io/library/mariadb:11@sha256:f16d7a344413e519dff518f27bdb221388f84b0d274c97c4fb5f2cb0dc1b34fa
|
||||||
Network=dankserver_appnet:alias=mariadb
|
Network=dankserver_appnet:alias=mariadb
|
||||||
EnvironmentFile=/home/dank/Containers/romm/env
|
EnvironmentFile=/home/dank/Containers/romm/env
|
||||||
Environment=MARIADB_DATABASE=romm
|
Environment=MARIADB_DATABASE=romm
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ After=network.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=romm-redis
|
ContainerName=romm-redis
|
||||||
Image=docker.io/library/redis:7-alpine
|
Image=docker.io/library/redis:7-alpine@sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499
|
||||||
Network=dankserver_appnet:alias=redis
|
Network=dankserver_appnet:alias=redis
|
||||||
Volume=/home/dank/Containers/romm/redis:/data
|
Volume=/home/dank/Containers/romm/redis:/data
|
||||||
HealthCmd=redis-cli ping
|
HealthCmd=redis-cli ping
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ After=romm-mariadb.service romm-redis.service network.target
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=romm
|
ContainerName=romm
|
||||||
Image=ghcr.io/rommapp/romm:latest
|
Image=ghcr.io/rommapp/romm:latest@sha256:03295f95a6290e752641f9553ac4198b0e5b71b879d57f7b2371b98f608feb54
|
||||||
Network=dankserver_appnet
|
Network=dankserver_appnet
|
||||||
EnvironmentFile=/home/dank/Containers/romm/env
|
EnvironmentFile=/home/dank/Containers/romm/env
|
||||||
Environment=DB_HOST=mariadb
|
Environment=DB_HOST=mariadb
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ After=network-online.target
|
|||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
Image=lscr.io/linuxserver/tautulli:v2.18.1-ls245
|
Image=lscr.io/linuxserver/tautulli:v2.18.1-ls245@sha256:c66d3afa8f7a701a48dd1b1f57b22eca0483d895d670fe11ecff783f6a9bbd73
|
||||||
ContainerName=tautulli
|
ContainerName=tautulli
|
||||||
Network=dankserver_appnet:alias=tautulli
|
Network=dankserver_appnet:alias=tautulli
|
||||||
Environment=PUID=0
|
Environment=PUID=0
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Description=Gluetun VPN tunnel (FrankBook-Pro podman machine)
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=gluetun
|
ContainerName=gluetun
|
||||||
Image=docker.io/qmcgaw/gluetun:latest
|
Image=docker.io/qmcgaw/gluetun:latest@sha256:c2ee4191762fca6b6a3c3359a731198a95d9c9af4db32d466e223bf3a84e779b
|
||||||
AddCapability=NET_ADMIN
|
AddCapability=NET_ADMIN
|
||||||
AddDevice=/dev/net/tun:/dev/net/tun
|
AddDevice=/dev/net/tun:/dev/net/tun
|
||||||
Environment=VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
|
Environment=VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ After=gluetun.service
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=lidarr
|
ContainerName=lidarr
|
||||||
Image=lscr.io/linuxserver/lidarr:latest
|
Image=lscr.io/linuxserver/lidarr:latest@sha256:d8549ff9ea29b4015c32257360652c1000c31ea471a2b3f0835ae77292faf35b
|
||||||
Environment=PUID=0
|
Environment=PUID=0
|
||||||
Environment=PGID=0
|
Environment=PGID=0
|
||||||
Environment=TZ=America/Los_Angeles
|
Environment=TZ=America/Los_Angeles
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ After=gluetun.service
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=prowlarr
|
ContainerName=prowlarr
|
||||||
Image=lscr.io/linuxserver/prowlarr:latest
|
Image=lscr.io/linuxserver/prowlarr:latest@sha256:b3b00047ed20f6ce2b16780145cb6a0acb51c9e28ea6aa6f5dcd33c7034f1797
|
||||||
Environment=PUID=0
|
Environment=PUID=0
|
||||||
Environment=PGID=0
|
Environment=PGID=0
|
||||||
Environment=TZ=America/Los_Angeles
|
Environment=TZ=America/Los_Angeles
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Network=gluetun.container
|
|||||||
# Pinned to the last 4.x line: Readarr's retired qB client requires the
|
# 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
|
# 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.
|
# 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=PUID=0
|
||||||
Environment=PGID=0
|
Environment=PGID=0
|
||||||
Environment=TZ=America/Los_Angeles
|
Environment=TZ=America/Los_Angeles
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ After=gluetun.service
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=radarr
|
ContainerName=radarr
|
||||||
Image=lscr.io/linuxserver/radarr:latest
|
Image=lscr.io/linuxserver/radarr:latest@sha256:fc0ae3c50f8128b97e84381903ec8d6845c3a569641cb7117c124376cccb57c1
|
||||||
Environment=PUID=0
|
Environment=PUID=0
|
||||||
Environment=PGID=0
|
Environment=PGID=0
|
||||||
Environment=TZ=America/Los_Angeles
|
Environment=TZ=America/Los_Angeles
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ After=gluetun.service
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=readarr
|
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=PUID=0
|
||||||
Environment=PGID=0
|
Environment=PGID=0
|
||||||
Environment=TZ=America/Los_Angeles
|
Environment=TZ=America/Los_Angeles
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ After=gluetun.service
|
|||||||
|
|
||||||
[Container]
|
[Container]
|
||||||
ContainerName=sonarr
|
ContainerName=sonarr
|
||||||
Image=lscr.io/linuxserver/sonarr:latest
|
Image=lscr.io/linuxserver/sonarr:latest@sha256:889dcc58c6aea859033985894b519b2e6b1e55d92527ed123f9f52e42ae7c97b
|
||||||
Environment=PUID=0
|
Environment=PUID=0
|
||||||
Environment=PGID=0
|
Environment=PGID=0
|
||||||
Environment=TZ=America/Los_Angeles
|
Environment=TZ=America/Los_Angeles
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ service_is_allowed() {
|
|||||||
dankclaw:open-webui|dankclaw:gitea-postgres|dankclaw:gitea-server|\
|
dankclaw:open-webui|dankclaw:gitea-postgres|dankclaw:gitea-server|\
|
||||||
dankclaw:comfyui|dankclaw:hermes|\
|
dankclaw:comfyui|dankclaw:hermes|\
|
||||||
dankclaw:hermes-dashboard|dankclaw:hermes-webui|dankclaw:prometheus|dankclaw:grafana|\
|
dankclaw:hermes-dashboard|dankclaw:hermes-webui|dankclaw:prometheus|dankclaw:grafana|\
|
||||||
dankclaw:loki|dankclaw:alloy|dankclaw:node-exporter|dankclaw:cadvisor|dankclaw:gpu-exporter|dankclaw:alertmanager|\
|
dankclaw:loki|dankclaw:alloy|dankclaw:node-exporter|dankclaw:cadvisor|dankclaw:gpu-exporter|dankclaw:alertmanager|dankclaw:blackbox-exporter|\
|
||||||
dankclaw:authentik-postgres|dankclaw:authentik-server|\
|
dankclaw:authentik-postgres|dankclaw:authentik-server|\
|
||||||
dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|\
|
dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|\
|
||||||
dankclaw:caddy|dankclaw:homepage|dankclaw:seerr|dankclaw:marreta|\
|
dankclaw:caddy|dankclaw:homepage|dankclaw:seerr|dankclaw:marreta|\
|
||||||
@@ -617,6 +617,8 @@ deploy_observability() {
|
|||||||
loki) wait_for_health loki http://127.0.0.1:3100/ready 60 ;;
|
loki) wait_for_health loki http://127.0.0.1:3100/ready 60 ;;
|
||||||
node-exporter) wait_for_health node-exporter http://127.0.0.1:9100/metrics 60 ;;
|
node-exporter) wait_for_health node-exporter http://127.0.0.1:9100/metrics 60 ;;
|
||||||
cadvisor) wait_for_health cadvisor http://127.0.0.1:8080/healthz 90 ;;
|
cadvisor) wait_for_health cadvisor http://127.0.0.1:8080/healthz 90 ;;
|
||||||
|
# Loopback: the exporter binds 127.0.0.1 + LAN (the gateway address
|
||||||
|
# is not bindable from the host netns — see the unit header).
|
||||||
blackbox-exporter) wait_for_health blackbox-exporter http://127.0.0.1:9115/-/healthy 60 ;;
|
blackbox-exporter) wait_for_health blackbox-exporter http://127.0.0.1:9115/-/healthy 60 ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -2086,7 +2088,7 @@ deploy_service() {
|
|||||||
plex) deploy_plex ;;
|
plex) deploy_plex ;;
|
||||||
tautulli) deploy_tautulli ;;
|
tautulli) deploy_tautulli ;;
|
||||||
alloy) deploy_alloy ;;
|
alloy) deploy_alloy ;;
|
||||||
prometheus|grafana|loki|node-exporter|cadvisor|alertmanager) deploy_observability "$service" ;;
|
prometheus|grafana|loki|node-exporter|cadvisor|alertmanager|blackbox-exporter) deploy_observability "$service" ;;
|
||||||
gpu-exporter) deploy_gpu_exporter ;;
|
gpu-exporter) deploy_gpu_exporter ;;
|
||||||
comfyui) deploy_comfyui ;;
|
comfyui) deploy_comfyui ;;
|
||||||
*) deploy_compose_service "$service" ;;
|
*) deploy_compose_service "$service" ;;
|
||||||
|
|||||||
+1
-1
@@ -140,7 +140,7 @@ valid_service() {
|
|||||||
dankclaw:gitea-server|dankclaw:homepage|\
|
dankclaw:gitea-server|dankclaw:homepage|\
|
||||||
dankclaw:comfyui|dankclaw:hermes|dankclaw:hermes-dashboard|\
|
dankclaw:comfyui|dankclaw:hermes|dankclaw:hermes-dashboard|\
|
||||||
dankclaw:hermes-webui|dankclaw:prometheus|dankclaw:grafana|dankclaw:loki|dankclaw:alloy|\
|
dankclaw:hermes-webui|dankclaw:prometheus|dankclaw:grafana|dankclaw:loki|dankclaw:alloy|\
|
||||||
dankclaw:alertmanager|\
|
dankclaw:alertmanager|dankclaw:blackbox-exporter|\
|
||||||
dankclaw:node-exporter|dankclaw:cadvisor|dankclaw:gpu-exporter|dankclaw:authentik-postgres|dankclaw:authentik-server|\
|
dankclaw:node-exporter|dankclaw:cadvisor|dankclaw:gpu-exporter|dankclaw:authentik-postgres|dankclaw:authentik-server|\
|
||||||
dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|dankclaw:seerr|\
|
dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|dankclaw:seerr|\
|
||||||
dankclaw:open-notebook|\
|
dankclaw:open-notebook|\
|
||||||
|
|||||||
@@ -130,10 +130,19 @@ Quickest unblock: `podman compose up -d --force-recreate <svc>`. Durable fix
|
|||||||
|
|
||||||
## 4. Podman 5.x quadlet v1 generator quirks (verified live 2026-08-28)
|
## 4. Podman 5.x quadlet v1 generator quirks (verified live 2026-08-28)
|
||||||
|
|
||||||
- **Hard-fail keys** — the whole unit silently fails to generate
|
- **Hard-fail keys abort the WHOLE BATCH, not one unit** (podman 5.7.0,
|
||||||
(`systemctl --user cat <unit>` shows "could not be found"): any
|
live 2026-10-21): one unsupported key — e.g. `HostAddr=` — makes the
|
||||||
dash-prefixed `[Container]` option (`--shm-size=…` → `unsupported key …
|
generator log `unsupported key … in group 'Container'` plus
|
||||||
in group 'Container'`); `Command=` is unsupported, use `Exec=`.
|
`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 <x>.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
|
- **HealthCmd space-splitting:** the v1 generator splits `HealthCmd` on
|
||||||
spaces into the healthcheck argv; a quoted probe
|
spaces into the healthcheck argv; a quoted probe
|
||||||
`/bin/sh -c "pg_isready …"` lands as argv token `"pg_isready` →
|
`/bin/sh -c "pg_isready …"` lands as argv token `"pg_isready` →
|
||||||
@@ -148,6 +157,16 @@ Quickest unblock: `podman compose up -d --force-recreate <svc>`. Durable fix
|
|||||||
"unsupported key"`, `systemctl --user cat probe.service` (shows the
|
"unsupported key"`, `systemctl --user cat probe.service` (shows the
|
||||||
generated ExecStart verbatim), then remove + reload.
|
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
|
## 5. Rootless Quadlet log retrieval
|
||||||
|
|
||||||
Step 0 — establish ownership: `systemctl --user status <name>.service`
|
Step 0 — establish ownership: `systemctl --user status <name>.service`
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ committed.
|
|||||||
| `shared/prometheus/rules/llm-fleet.yml` | alert rules + recording rules |
|
| `shared/prometheus/rules/llm-fleet.yml` | alert rules + recording rules |
|
||||||
| `shared/grafana/provisioning/` | datasources + dashboards (provisioned tree) |
|
| `shared/grafana/provisioning/` | datasources + dashboards (provisioned tree) |
|
||||||
| `shared/alloy/config.alloy` | the log pipeline: journal source → label promotion → Loki push |
|
| `shared/alloy/config.alloy` | the log pipeline: journal source → label promotion → Loki push |
|
||||||
| `shared/blackbox/blackbox.yml` | prober modules (`http_2xx`) + the loopback bind |
|
| `shared/blackbox/blackbox.yml` | prober modules (`http_2xx`); the listen bind lives on the unit's `Exec` |
|
||||||
|
|
||||||
Targets and datasources reference **container names** (`grafana:3000`
|
Targets and datasources reference **container names** (`grafana:3000`
|
||||||
`loki:3100` — `shared/prometheus/prometheus.yml:52-61`), never
|
`loki:3100` — `shared/prometheus/prometheus.yml:52-61`), never
|
||||||
|
|||||||
@@ -163,6 +163,6 @@ verify it through the APIs.
|
|||||||
- `scripts/gpu-exporter.py`, `scripts/gpu-exporter.service` (per-GPU host exporter)
|
- `scripts/gpu-exporter.py`, `scripts/gpu-exporter.service` (per-GPU host exporter)
|
||||||
- `shared/prometheus/prometheus.yml`, `shared/prometheus/alertmanager.yml`, `shared/prometheus/rules/llm-fleet.yml`
|
- `shared/prometheus/prometheus.yml`, `shared/prometheus/alertmanager.yml`, `shared/prometheus/rules/llm-fleet.yml`
|
||||||
- `shared/grafana/provisioning/`, `shared/loki/loki-config.yaml`, `shared/alloy/config.alloy`
|
- `shared/grafana/provisioning/`, `shared/loki/loki-config.yaml`, `shared/alloy/config.alloy`
|
||||||
- `shared/blackbox/blackbox.yml` (prober modules + loopback bind)
|
- `shared/blackbox/blackbox.yml` (prober modules; the listen bind is on the unit's `Exec`)
|
||||||
- `tests/observability_quadlet.bats` (boot persistence, no wildcard publish, probe addresses, delivery contract)
|
- `tests/observability_quadlet.bats` (boot persistence, no wildcard publish, probe addresses, delivery contract)
|
||||||
- Host evidence 2026-10-18: `ss -ltnp`, `podman ps`, `systemctl --user is-enabled|is-active`, `curl` on `127.0.0.1:9090/api/v1/{targets,rules,alerts}`, `127.0.0.1:3100/loki/api/v1/labels`, `127.0.0.1:12345/metrics`, `127.0.0.1:3002/api/health`, `192.168.0.143:18093/health`, `journalctl --user -u signal-relay`
|
- Host evidence 2026-10-18: `ss -ltnp`, `podman ps`, `systemctl --user is-enabled|is-active`, `curl` on `127.0.0.1:9090/api/v1/{targets,rules,alerts}`, `127.0.0.1:3100/loki/api/v1/labels`, `127.0.0.1:12345/metrics`, `127.0.0.1:3002/api/health`, `192.168.0.143:18093/health`, `journalctl --user -u signal-relay`
|
||||||
@@ -48,16 +48,18 @@ retired 2026-10-21; census re-run after each deploy):
|
|||||||
| Job | Target | how |
|
| Job | Target | how |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `gitea` | `192.168.0.143:3000/metrics` | metrics enabled in the quadlet (`GITEA__metrics__ENABLED=true`, `ENABLED_HOST=0.0.0.0`) — the page once said "stays off until `[metrics] ENABLED = true` in its own config"; the env form is what the deployer ships |
|
| `gitea` | `192.168.0.143:3000/metrics` | metrics enabled in the quadlet (`GITEA__metrics__ENABLED=true`, `ENABLED_HOST=0.0.0.0`) — the page once said "stays off until `[metrics] ENABLED = true` in its own config"; the env form is what the deployer ships |
|
||||||
| `services` | `http://192.168.0.143:8081` (SearXNG), `:3005` (OpenWebUI), `http://comfyui:8188` | **blackbox exporter** probe, module `http_2xx` — the comfyui metrics job is retired, one owner per service |
|
| `services` | `http://192.168.0.143:8081` (SearXNG), `:3005` (OpenWebUI), `http://127.0.0.1:8188` (ComfyUI) | **blackbox exporter** probe, module `http_2xx` — the comfyui metrics job is retired, one owner per service. The prober runs in the host netns, so comfyui is probed over loopback (its published port); bridge container names do not resolve there |
|
||||||
|
|
||||||
Scraping a web root is not a health check: an HTML page fails the scrape
|
Scraping a web root is not a health check: an HTML page fails the scrape
|
||||||
protocol (`unsupported Content-Type`), which is what kept those targets red
|
protocol (`unsupported Content-Type`), which is what kept those targets red
|
||||||
beside healthy services. The classification rule and the honesty law live in
|
beside healthy services. The classification rule and the honesty law live in
|
||||||
[[Stale Signal Audit]]. The prober is `blackbox-exporter.container` (host
|
[[Stale Signal Audit]]. The prober is `blackbox-exporter.container` (host
|
||||||
netns, config `shared/blackbox/blackbox.yml`, bound 127.0.0.1:9115; Prometheus
|
netns, config `shared/blackbox/blackbox.yml`); it binds 9115 on loopback and
|
||||||
reaches it at the `host.containers.internal` gateway name). Plain targets stay
|
the LAN, and Prometheus scrapes it at `host.containers.internal` — traffic to
|
||||||
`host:port`, never URLs — the probe job's URL targets are the documented
|
that gateway lands on the host's loopback listener. The gateway address is
|
||||||
exception, relabeled onto the prober.
|
not itself bindable from the host netns.
|
||||||
|
Plain targets stay `host:port`, never URLs — the probe job's URL targets are
|
||||||
|
the documented exception, relabeled onto the prober.
|
||||||
|
|
||||||
**`frank-zealot` reads up since 2026-10-18, and did not before.** llama.cpp only answers `/metrics` when started with `--metrics`, so `up{job="frank-zealot"}` was 0 across the whole retained history (the oldest sample, 2026-10-11, is already 0) while the service answered `/health` 200 the whole time; the flag and its opt-out are on [[frank-zealot]]. Read `lastError` before believing an outage: a target can read down beside a healthy service.
|
**`frank-zealot` reads up since 2026-10-18, and did not before.** llama.cpp only answers `/metrics` when started with `--metrics`, so `up{job="frank-zealot"}` was 0 across the whole retained history (the oldest sample, 2026-10-11, is already 0) while the service answered `/health` 200 the whole time; the flag and its opt-out are on [[frank-zealot]]. Read `lastError` before believing an outage: a target can read down beside a healthy service.
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,10 @@
|
|||||||
# 200 that fails the scrape protocol), which is why the old `services` and
|
# 200 that fails the scrape protocol), which is why the old `services` and
|
||||||
# `comfyui` jobs sat permanently red on `unsupported Content-Type`.
|
# `comfyui` jobs sat permanently red on `unsupported Content-Type`.
|
||||||
# See serverAppConfigs-docs/Architecture/Stale Signal Audit.md §1.
|
# See serverAppConfigs-docs/Architecture/Stale Signal Audit.md §1.
|
||||||
# Loopback bind: the image's default 0.0.0.0:9115 would ride the LAN on this
|
# The listen address is NOT set here: the quadlet unit's Exec passes
|
||||||
# Network=host container. Prometheus (bridge) still reaches it — its traffic
|
# --listen-address (LAN bind — see the unit header for why loopback is
|
||||||
# arrives on the podman gateway interface, which is not loopback.
|
# unreachable from the bridge-networked Prometheus). Keep this file to
|
||||||
network:
|
# modules only, so the two sources cannot disagree.
|
||||||
listen_address: 127.0.0.1:9115
|
|
||||||
|
|
||||||
modules:
|
modules:
|
||||||
http_2xx:
|
http_2xx:
|
||||||
|
|||||||
@@ -64,11 +64,13 @@ scrape_configs:
|
|||||||
# web root is NOT a health check — an HTML front page fails the scrape
|
# web root is NOT a health check — an HTML front page fails the scrape
|
||||||
# protocol (`unsupported Content-Type`), which is exactly why the old
|
# protocol (`unsupported Content-Type`), which is exactly why the old
|
||||||
# `services` and `comfyui` jobs were red forever beside healthy services
|
# `services` and `comfyui` jobs were red forever beside healthy services
|
||||||
# (Stale Signal Audit §1). The prober is a host-netns container reached at
|
# (Stale Signal Audit §1). The prober is a host-netns container listening on
|
||||||
# the podman gateway name (HostAddr= on the prometheus unit); it in turn
|
# the podman gateway (the scrape address below - the netavark hairpin gap
|
||||||
# reaches both the host LAN IP and bridge container names. comfyui's UI probe
|
# bars this container from its own host's LAN IP, cadvisor job note) and on
|
||||||
# lives here now — its own metrics-scrape job is retired, one owner per
|
# the LAN; from that netns it reaches the host LAN IP, and published ports
|
||||||
# service.
|
# via 127.0.0.1 - bridge container names do NOT resolve in the host netns,
|
||||||
|
# which is why comfyui's probe is a loopback URL. comfyui's own
|
||||||
|
# metrics-scrape job is retired, one owner per service.
|
||||||
- job_name: "services"
|
- job_name: "services"
|
||||||
metrics_path: /probe
|
metrics_path: /probe
|
||||||
params:
|
params:
|
||||||
@@ -77,7 +79,7 @@ scrape_configs:
|
|||||||
- targets:
|
- targets:
|
||||||
- "http://192.168.0.143:8081" # SearXNG (via the Gluetun tunnel)
|
- "http://192.168.0.143:8081" # SearXNG (via the Gluetun tunnel)
|
||||||
- "http://192.168.0.143:3005" # OpenWebUI
|
- "http://192.168.0.143:3005" # OpenWebUI
|
||||||
- "http://comfyui:8188" # ComfyUI UI (gfx1151 generator)
|
- "http://127.0.0.1:8188" # ComfyUI UI (published 0.0.0.0:8188; probed from the prober's host netns)
|
||||||
relabel_configs:
|
relabel_configs:
|
||||||
- source_labels: [__address__]
|
- source_labels: [__address__]
|
||||||
target_label: __param_target
|
target_label: __param_target
|
||||||
|
|||||||
@@ -150,3 +150,23 @@ load helpers
|
|||||||
grep -q "$pair" "$BATS_TEST_TMPDIR/cli" || { echo "client gate missing $pair"; return 1; }
|
grep -q "$pair" "$BATS_TEST_TMPDIR/cli" || { echo "client gate missing $pair"; return 1; }
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "every gated dankclaw service is dispatched" {
|
||||||
|
# A service can clear both allowlists and still die inside the deployer:
|
||||||
|
# dankclaw has no compose file, and deploy_compose_service (the case
|
||||||
|
# catch-all) hard-fails with "no quadlet handler" since 2026-09-16. So
|
||||||
|
# every name the remote gate allows for dankclaw MUST be named by a
|
||||||
|
# deploy_service pattern — blackbox-exporter (2026-10-21) shipped with
|
||||||
|
# both gates and the health gate wired but no dispatch case, and only
|
||||||
|
# the deploy attempt found it.
|
||||||
|
run env OUT="$BATS_TEST_TMPDIR" bash -c '
|
||||||
|
grep -oE "dankclaw:[a-z0-9-]+" \
|
||||||
|
<(sed -n "/^service_is_allowed()/,/^}/p" "$1/scripts/deploy-remote.sh") \
|
||||||
|
| cut -d: -f2 | sort -u > "$OUT/gated"
|
||||||
|
sed -n "/^deploy_service()/,/^}/p" "$1/scripts/deploy-remote.sh" \
|
||||||
|
| grep -oE "^ +[a-z][a-z0-9|-]*\)" | tr -d " )" | tr "|" "\n" \
|
||||||
|
| sort -u > "$OUT/dispatched"
|
||||||
|
comm -23 "$OUT/gated" "$OUT/dispatched"
|
||||||
|
' _ "$REPO_ROOT"
|
||||||
|
[ -z "$output" ] || { echo "gated dankclaw services with no deploy_service case:"; echo "$output"; return 1; }
|
||||||
|
}
|
||||||
@@ -77,6 +77,7 @@ FRANKBOOK_UNITS=(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@test "readarr keeps its pinned image" {
|
@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
|
"$REPO_ROOT/frankbook-pro/readarr/quadlet/readarr.container" || return 1
|
||||||
}
|
}
|
||||||
@@ -669,6 +669,18 @@ assert not any('container_network' in e or 'container_fs_' in e for e in names),
|
|||||||
[ -f "$sandbox/opt/grafana/provisioning/datasources/prometheus.yml" ]
|
[ -f "$sandbox/opt/grafana/provisioning/datasources/prometheus.yml" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "every observability unit declares an Image" {
|
||||||
|
# Quadlet refuses to GENERATE a service for a .container without Image=
|
||||||
|
# - the deployer then dies at `systemctl restart` with "Unit not found"
|
||||||
|
# and no diagnosis (blackbox-exporter, live 2026-10-21: merged with the
|
||||||
|
# copier, gates and health check wired, but no Image line).
|
||||||
|
local svc
|
||||||
|
for svc in "${UNITS[@]}"; do
|
||||||
|
grep -q '^Image=' "$UNIT_DIR/$svc.container" \
|
||||||
|
|| { echo "$svc.container has no Image= - quadlet will not generate it"; return 1; }
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
@test "changes under shared/blackbox/ plan the prober" {
|
@test "changes under shared/blackbox/ plan the prober" {
|
||||||
run python3 "$REPO_ROOT/scripts/deploy-plan.py" --target dankclaw \
|
run python3 "$REPO_ROOT/scripts/deploy-plan.py" --target dankclaw \
|
||||||
--paths shared/blackbox/blackbox.yml
|
--paths shared/blackbox/blackbox.yml
|
||||||
@@ -716,7 +728,7 @@ assert not any('container_network' in e or 'container_fs_' in e for e in names),
|
|||||||
copy_blackbox_config
|
copy_blackbox_config
|
||||||
)
|
)
|
||||||
[ -f "$sandbox/blackbox.yml" ]
|
[ -f "$sandbox/blackbox.yml" ]
|
||||||
grep -q "listen_address: 127.0.0.1:9115" "$sandbox/blackbox.yml"
|
grep -q '^ http_2xx:' "$sandbox/blackbox.yml"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "the alloy copier refuses to install a config the engine rejects" {
|
@test "the alloy copier refuses to install a config the engine rejects" {
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ QDIR="$REPO_ROOT/dankclaw/open-webui/quadlet"
|
|||||||
}
|
}
|
||||||
|
|
||||||
@test "open-webui uses the CPU image, not :cuda (dankclaw is CPU-only)" {
|
@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" \
|
# 2026-10-21 fleet law: tag AND digest (tests/quadlet_image_pins.bats)
|
||||||
|| { echo "open-webui.container must use the :main (CPU) image"; return 1; }
|
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
|
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"
|
echo "open-webui.container must not use the :cuda image on a CPU-only host"
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
Executable
+47
@@ -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; }
|
||||||
|
}
|
||||||
@@ -20,8 +20,9 @@ SEERR_UNIT="$REPO_ROOT/dankclaw/seerr/quadlet/seerr.container"
|
|||||||
grep -q '^Restart=always$' "$SEERR_UNIT"
|
grep -q '^Restart=always$' "$SEERR_UNIT"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "seerr image is pinned to an exact version tag" {
|
@test "seerr image is pinned to an exact version tag and digest" {
|
||||||
grep -Eq '^Image=ghcr\.io/seerr-team/seerr:v[0-9]+\.[0-9]+\.[0-9]+$' "$SEERR_UNIT"
|
# 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)" {
|
@test "seerr publishes 5055 on loopback only (guard chain is the user path)" {
|
||||||
|
|||||||
Reference in new issue
Block a user