4.5 KiB
4.5 KiB
name, description
| name | description |
|---|---|
| compose-to-quadlet-cutover | Convert a compose-only service to a boot-persistent rootless Quadlet unit on the Dankclaw/Dankserver fleet — checklist plus the traps (CMD-SHELL healthchecks in distroless images, --format docker, EnvironmentFile HOME=/root, loopback publish posture) |
Compose service → boot-persistent Quadlet unit (this fleet)
Use when a service in dankclaw/compose.yaml (or dankserver/) must survive a
reboot. Precedents to copy: dankclaw/{caddy,gitea,searxng,open-webui,seerr}/quadlet/
and dankclaw/observability/quadlet/ (five units, 2026-09-11).
Checklist
- Unit file at
dankclaw/<svc>/quadlet/<name>.container, mirroring the compose service line for line:Image=,ContainerName=<the compose name>(Caddy and scrape configs address containers by name),Network=dankclaw_default,User=0:0when the image's own user cannot write a rootless-owned bind mount,Volume=for each mount (:ropreserved),Exec=for the composecommand:,PublishPort=,HealthCmd/HealthInterval/HealthTimeout/HealthRetries/HealthStartPeriod,[Service] Restart=always+RestartSec=10,[Install] WantedBy=default.target. Neversystemctl --user enablethe generated unit —[Install]plus linger IS the boot persistence. - Exposure: publish on
127.0.0.1unless something off-host genuinely needs it. Caddy reaches containers by name over the shared network, and the homepage status timer probes loopback, so the 0.0.0.0 publish is almost always gratuitous and is a guard-chain bypass. - Handler in
scripts/deploy-remote.sh:install_quadlet <dir> <name>→systemctl --user restart <name>.service→wait_for_healthon the loopback endpoint. Rebuild locally built images first (build_compose_image), and seed any per-service env file (see traps). - Planner: map
dankclaw/<svc>/*andshared/<svc>/*to the service inscripts/deploy-plan.py; keep dependencies ordered inSERVICE_ORDER. Add ascripts/test-changed.shcase above the genericdankclaw/*/quadlet/*.containerfallback. - Retire the compose entry in the same change — two owners for one container name is how a stale duplicate comes back.
- Seam test
tests/<svc>_quadlet.bats: boot persistence, restart policy, no wildcard publish, container name still matching Caddy/scrape references, health command, deployer installs+restarts+waits, neverrm, neverenable. Migrate any older assertion that read the compose file. - Deploy per service (
./scripts/deploy.sh dankclaw --service <name>), then prove:is-active/is-enabled= active/generated, container healthhealthy, the endpoint answers,list-dependencies default.targetshows the unit, and off-host probes on the previously published ports now refuse.
Traps that cost real time
HealthCmd= CMD-SHELL. podman runs it as/bin/sh -c; a distroless image has no shell, so the check fails forever.--health-cmdwill not take exec form either (a JSON array is stored as one literal argument). PutHEALTHCHECK ... CMD ["binary","arg"]in the image and build with--format docker— the default OCI format has no HEALTHCHECK field and drops it with only a warning.- Never point a unit's
EnvironmentFileat the repo root.env. That file is a gluetun env file carryingHOME=/root: systemd sets HOME for the service, podman then looks for its config under/root, and the unit dies withstat /root/.config: permission denied. Use a per-service file under/home/dank/Containers/<svc>/env, seeded by the deploy handler with a single-variable filter and validated so a missing secret fails loudly. Network=hostexporters that a containerized Prometheus scrapes must bind the host's LAN address (containers cannot reach the host loopback) plus127.0.0.1for the homepage probe — two--web.listen-addressflags, never the wildcard.- alloy's UI listens on the container's loopback and 1.x refuses the
uiconfig block, so the unit must pass--server.http.listen-addr=0.0.0.0:12345before its port can be published; without it the service has no reachable health endpoint and its status card reads down forever. - Locally built images need
Pull=neverandImage=localhost/<repo>/<name>:tag; registry-prefixed refs do not resolve from local storage on these hosts. - Compose-managed containers are replaced in place by
podman run --replace --rm; verify afterwards that the container no longer carriesio.podman.compose.project.