A container registry is infrastructure, not just image storage, and it gets treated as an afterthought more often than any other piece of the deploy pipeline. Tag hygiene, access control, and retention policy all decay quietly until a rollback fails because the image you need was garbage-collected, or a compromised CI token pushes to a registry with no scanning and no audit trail. None of the fixes are exotic; they just need to be decided on purpose instead of by default.
Stop using mutable tags for deploys
latest is the most common source of "it worked yesterday" incidents. It is a mutable pointer — pushing a new image under the same tag silently replaces what latest resolves to, which means a rollback to "the previous latest" is not actually possible once a new push has happened. Deploy by immutable reference: either a semantic version tag that's never reused, or better, the image digest (registry/app@sha256:...), which is content-addressed and can never point to different content. CI should resolve and record the digest at build time and pass that digest through to the deploy step, not the tag.
- name: Build and push
id: push
uses: docker/build-push-action@v6
with:
push: true
tags: registry.example.com/app:${{ github.sha }}
- name: Record digest for deploy
run: echo "IMAGE_DIGEST=${{ steps.push.outputs.digest }}" >> "$GITHUB_ENV"
Access control and least-privilege push rights
Anyone or anything that can push to a production registry can, intentionally or via a compromised credential, ship malicious code straight to your clusters. Scope push access to CI service accounts only, never to individual developer credentials, and scope those service accounts to the specific repository paths they need — a service that builds one app shouldn't have push rights to every namespace in the registry. Pull access can usually be broader, but even that should be scoped per-environment if the registry holds anything sensitive.
If that credential leaks — in a log, a misconfigured CI variable, a compromised dependency — the blast radius is every image behind it. Rotate registry credentials on a schedule, use short-lived tokens (OIDC federation to the registry, where supported) instead of long-lived static keys, and alert on pushes from unexpected sources.
Vulnerability scanning at push time
Most managed registries (ECR, GAR, ACR, Docker Hub, GitHub Container Registry, Harbor self-hosted) support scanning images on push using Trivy, Grype, or a vendor-native scanner. Wire the scan result into the pipeline as a gate for at least critical and high severity findings on the base OS layer — not necessarily blocking every build on every finding, since some are unfixable or false positives, but blocking silently shipping a known-critical CVE with an available fix.
Retention and garbage collection
Registries fill up, and the instinct to "just keep everything" runs into both storage cost and, more importantly, into the case where you actually need an old image for a rollback and it's gone because someone ran an aggressive cleanup script without understanding the retention needs. A sane default: keep every tagged release image indefinitely (they're small relative to the value of being able to roll back), and garbage-collect untagged/dangling layers and CI-branch images (like `pr-1234`) after a short window — a week or two is usually enough.
An image can be untagged in the registry's tag list but still be the exact digest a running deployment references. Registry garbage collection should check against what's actually deployed, or at minimum you need a long enough retention window that a stuck rollback doesn't hit a wall.
Signing and provenance
Image signing (Sigstore/Cosign is the common open-source path) lets a cluster admission controller verify that an image was built by your CI pipeline and hasn't been tampered with or swapped for something pushed by an unauthorized party. This is worth adopting once you have the basics above solid — access control, immutable references, and scanning — since signing without those in place mostly signs a pipeline that could already be compromised at an earlier step.
| Practice | Prevents |
|---|---|
| Deploy by digest, not mutable tag | Silent image swaps, broken rollbacks |
| Scoped, short-lived push credentials | Supply-chain compromise via leaked credentials |
| Scan-at-push with a severity gate | Known-critical CVEs reaching production |
| Retention policy that checks deployed digests | Garbage-collecting an image you still need |
| Image signing (Cosign/Sigstore) | Unauthorized or tampered images passing admission |
Wrapping up
Registry hygiene is unglamorous work that mostly pays off during an incident: the rollback that actually works because you deployed by digest, the compromised token that couldn't reach production because it was scoped to one repo, the CVE that got caught before it shipped. Fix the mutable-tag problem first — it's the cheapest change with the most direct effect on whether a rollback is actually possible when you need one.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.