5.2 KiB
REPORT — image pinning: :latest eliminated from the catalog (2026-07-12)
Class: implementation (template change + sweep gate; push = deploy).
Baseline: main @ 2ebe082 → (this commit). Scope: 5 compose image lines + new gate script
- CLAUDE.md rule. No healthcheck/env/format touch-ups; no controller change; no redeploys triggered.
Why
The controller's recovery-unit ImagePins (felhom-controller internal/backup/recovery_unit.go)
pins the compose tag: restoring a :latest-pinned app re-pulls whatever :latest points to at
restore time — potentially schema-incompatible with the data being restored. Load-bearing rule:
a deployed app pins to the digest it is currently running (the pin never causes a version jump);
undeployed apps pin to the latest verified upstream stable.
The five pins (old → new, with evidence)
| App | New pin | Evidence (all gathered 2026-07-12) |
|---|---|---|
| calibre-web (DEPLOYED, demo 9201) | crocodilestick/calibre-web-automated:v4.0.6 |
Running RepoDigest on 9201: sha256:c31a738b6d5e…; Docker Hub tag v4.0.6 digest: sha256:c31a738b6d5e… — exact match (Scenario A: pin == running image; a newer tag would have violated the no-silent-upgrade guard) |
| bentopdf (undeployed) | ghcr.io/alam00000/bentopdf:v2.8.6 |
GitHub latest release v2.8.6 (2026-06-28); ghcr manifest digest sha256:eaeea1e4… == latest |
| papra (undeployed) | ghcr.io/papra-hq/papra:26.6.1-rootless |
ghcr latest digest sha256:a7a42e22… == 26.6.1-rootless; the -root variant differs (c5757540…) — the rootless variant is what :latest was shipping, so behavior (permission semantics) is preserved exactly |
| recipe-importer (own image, undeployed) | gitea.dooplex.hu/admin/recipe-importer:v0.9.11 |
Its repo CHANGELOG declares v0.9.11 (2026-02-26); the registry already had v0.9.11 (build.sh pushes :VERSION+:latest together), digest sha256:f3cb617c… == latest — Scenario C's retag was unnecessary; no push performed |
| termix (undeployed) | ghcr.io/lukegus/termix:2.5.0 |
ghcr digests: latest == 2.5.0 == release-2.5.0 (sha256:4d337131…); bare CalVer tag chosen |
All five docker manifest inspect <ref> checks ran on the build server (180) with per-command exit
codes checked — 5× OK, zero pipe-swallowed.
templates.json: contains no image strings (legacy Portainer-era metadata) — untouched, per §4.3.
Sweep + red-proofs (§10 / Scenario D)
- New rerunnable gate:
scripts/check-image-pins.py— scans everytemplates/*/docker-compose.ymlimage:line; fails on floating tags (latest,dev,nightly,edge,main,master) AND on untagged refs (implicit :latest);@sha256:counts as pinned; registry-port refs handled. - Final run:
image-pin gate OK — 53 templates, 0 unpinned images, exit 0. - Red-proof 1 (floating tag): reverted termix to
:latest→ exit 1 withtemplates/termix/docker-compose.yml:11 … [floating tag :latest]→ restored → exit 0. ✓ - Red-proof 2 (bare shape): stripped termix's tag entirely → exit 1 with
[NO TAG (implicit :latest)]→ restored → exit 0. ✓ (Both §7-D shapes proven.)
Fleet-deployment verification (Scenario B)
- demo 9201:
docker ps— bentopdf/papra/termix/recipe-importer not deployed; calibre-web running from:latest(Up 4 hours at check time). - felhotest (router.abonet.hu:33022): unreachable (connection refused).
- Peti's box: offline (dead fan, no ETA).
- Operator asked and approved proceeding: every pin is digest-identical to current
:latest, so even a hidden deployment elsewhere sees zero change vs. the status quo on its next redeploy.
Git-sync no-op verification on 9201
- Pushed
71828a8; sync triggered viadocker exec felhom-controller curl -s -X POST http://127.0.0.1:8080/api/sync→ HTTP 200. (Gotcha confirmed: guest-netns127.0.0.1:8080is NOT reachable — the API must be curled from inside the controller container; the public-URL POST proxy gotcha also applies.) - Synced file on 9201:
/opt/docker/stacks/calibre-web/docker-compose.yml:21now readsimage: crocodilestick/calibre-web-automated:v4.0.6. ✓ - calibre-web container after sync:
crocodilestick/calibre-web-automated:latest | Up 5 hours (healthy)— uptime unchanged, no restart; the running image's digest equals the v4.0.6 pin, so the next redeploy pulls the byte-identical image. ✓
Observations (not acted on)
- When Peti's box returns: check whether bentopdf/papra/termix/recipe-importer are deployed there; if any is running an OLDER digest than the pin, decide upgrade vs. per-node pin then. felhotest should get the same check when reachable again.
- papra upstream publishes only
-root/-rootlessvariant tags — any future manual upgrade must keep the-rootlesssuffix or permissions semantics change. - bentopdf's ghcr
tags/listAPI returns a stale/partial page (newest listed was 1.15.3 while v2.8.6 exists and serves) — tag existence must be checked via manifest HEAD/inspect, not tags/list. - recipe-importer's registry
:latestremains published (allowed; only templates must not use it).