Recipes

Rebuild a container image on public pinned bases to drop a retiring dev-base dependency

Discovered from README cross-links · today

Software & DevOps

Swapped a private dev-base image for pinned public bases and verified locally.

What they did

The sirens-echo Dockerfile had three stages all starting FROM ghcr.io/coilyco/agentic-os:release, an image nothing publishes and which is being retired. Publishing it would need a workflow edit the available credential cannot make, so the author instead rebuilt the image on public, pinned bases. They audited what each stage actually used from dev-base: the build stage only needed the Go toolchain, so it moved to golang:1.25.4-bookworm to match go.mod. The compose stage needed git for the catalogue clone plus bash, awk, find, cmp, coreutils, python3 for the roster JSON, and agent-compose. The shipped image needed the static Go binaries and the same tools, because deploy's compose-review.sh runs /app/scripts/stage-compose-sources.sh from the published image, so a distroless runtime would break that consumer. They confirmed ward, stdio MCP servers, node, dotnet, rust, the agent CLIs and the cloud CLIs were not used at runtime. They added new agent-compose and tools stages on debian:bookworm-20261027-slim, with compose and the shipped image both starting from tools and compose adding git only. Both bases are pinned by version and digest as multi-arch indexes. agent-compose is the already vendored 2.218.0 with checksums, and scripts/install-agent-compose.sh picks the arm64 build on arm64 using the release SHA256SUMS. They updated .dockerignore so the installer enters the build context, and removed dev-base references from AGENTS.md and two docs. No workflow edit was needed because the existing publish-echo-image job in ci.yml already pushes ghcr.io/coilyco/sirens-echo:<sha> on main pushes.

How it went

Local arm64 Docker Desktop build via just image exited 0, with 14 roster roles and 1 community role baked and sirens-echo-prompt --bundles passing. The image is 318 MB versus 6.14 GB for the cached dev-base image, so the reversal condition in the ticket does not apply. Each shipped binary run with no configuration fails with its own config or usage message and no loader error; sirens-echo with SIRENS_ECHO_ROLE=prod-manager gets past bundle lookup and stops on missing Discord settings. sirens-echo-definition-check passes on both shipped definitions. Deploy's compose-review.sh steps (docker create, docker cp, docker start) replayed against the image as the non-root user exited 0 with 15 bundles written. just build, policy-check, vet, test and image-publish-check all pass.

Worth knowing

The amd64 build was not verified locally; it only runs in CI via image-build. The pre-commit config still names deleted Forgejo hooks, so the commit ran local hooks only. Anonymous pull of the published package depends on GHCR visibility, which has no API and was to be checked after the first publish. apt packages (python3, git, ca-certificates) are named but not version-pinned, so they float within bookworm.

Wiring

Order: audit stage-by-stage base usage, then rewrite the Dockerfile so build uses golang:1.25.4-bookworm and new agent-compose and tools stages sit on debian:bookworm-20261027-slim pinned by digest. compose and the shipped image both FROM tools; compose adds git. Add scripts/install-agent-compose.sh that detects arm64 vs amd64 and verifies against the release SHA256SUMS, and update .dockerignore to include it. Keep the existing ci.yml publish-echo-image job unchanged since it already pushes ghcr.io/coilyco/sirens-echo:<sha> on main. Validate with just image, just build, just policy-check, just vet, just test and just image-publish-check, then replay deploy's compose-review.sh docker create/cp/start sequence as a non-root user.

Try it yourself

Audit my Dockerfile for private or unpublishable base images. For each stage, list exactly which tools and packages it uses from the base, then propose public bases pinned by version and digest (multi-arch index) that cover only those needs. Keep any runtime tools that downstream consumers invoke from the published image, and avoid distroless if scripts are executed inside it. Add a checksum-verified installer script for any vendored binary, make sure .dockerignore lets it into the build context, and remove stale references to the old base from docs. Then build locally, compare image size against the old base, run each shipped binary with no config to confirm it fails with its own usage message rather than a loader error, and replay any deploy-side docker create/cp/start steps as a non-root user.

Source

Tools in this stack