skip to content

You own image naming for an organisation with dozens of services and multiple environments. Design the tagging scheme — which tags exist, which may move, and what deployments actually reference — and justify the tradeoffs.

level: principalimportance: should knowfreq 35%

answer

  1. two classes: immutable vs rolling
  2. sha-<commit> + semver, tag-immutability on
  3. never encode env in the tag
  4. deploy by digest; env state lives in git
  5. retention: prune sha-*, keep releases + referenced digests

basics

~20 s

Split tags into immutable and rolling. Immutable: one per build, carrying the commit SHA and a semver release tag, never repushed. Rolling: 1, 1.4, stable, latest — convenience pointers only. Deployments reference digests; environments are recorded in a config repo, not encoded in image tags.

solid answer

~50 s

Two classes of tag, with an enforced rule that they never blur. **Immutable (deploy-eligible):** `myapp:1.4.2` and `myapp:sha-9f3c1ab` (optionally `1.4.2-rc.3` for pre-releases). One build, pushed once, registry tag-immutability turned on so a repush is rejected. These map one-to-one to a commit and an artifact. **Rolling (human convenience):** `1`, `1.4`, `stable`, `latest`. They move by design and must never appear in a Dockerfile `FROM`, a deployment manifest, or an admission-allowed reference. **Deployments reference digests.** CI resolves the release tag to its digest once and records it; admission policy rejects any workload image without `@sha256:`. **Do not encode environment in the tag** — no `myapp:prod`. Promotion then means repushing or repointing, which destroys artifact identity. Environment lives in the config repo that says "prod runs digest X". Optionally promote by *copying* into a production repository, which changes access control and retention rather than the artifact. The cost is discipline and tooling; the payoff is that every environment's contents are answerable from a git history.

code

bash · 18 lines
bash
REPO=registry.example.com/payments/ledger-api
VERSION=1.4.2
SHA=$(git rev-parse --short HEAD)

# refuse to overwrite an existing release tag
if crane digest "$REPO:$VERSION" >/dev/null 2>&1; then
  echo "tag $VERSION already exists — releases are immutable"; exit 1
fi

docker buildx build --platform linux/amd64,linux/arm64 \
  -t "$REPO:$VERSION" -t "$REPO:sha-$SHA" --push .

# rolling convenience pointers (humans only, never deployed)
docker buildx imagetools create -t "$REPO:1.4" "$REPO:$VERSION"
docker buildx imagetools create -t "$REPO:1"   "$REPO:$VERSION"

DIGEST=$(crane digest "$REPO:$VERSION")
echo "image: $REPO@$DIGEST  # $VERSION / $SHA" >> env/prod/ledger-api.yaml

go deeper

for a junior

Recall the shape: version tags plus a commit-SHA tag, and never deploy latest.

for a middle

Distinguish immutable from rolling tags, explain registry tag-immutability, and know that deployments should carry digests.

for a senior

Design the CI flow end to end — build, immutable push, digest resolution, config-repo commit, admission policy — and handle retention and promotion copies.

for a principal

Own the tradeoffs: artifact identity versus placement, cross-registry trust boundaries, retention and GC interactions with digest pins, tag-space scale, and where semver ceremony is worth its cost.

## The decision the scheme has to make Every image reference in the organisation answers one of three questions, and a good scheme keeps them separate: 1. *Which artifact is this?* — needs an immutable name. 2. *Which version line does it belong to?* — needs a human-meaningful, possibly moving name. 3. *What is running where?* — needs a record, not a tag. Most bad schemes fail by making one tag do all three. `myapp:prod` is the classic: it is simultaneously an identity, a version, and an environment pointer, and because it must move on every release it can be none of them reliably. ## The two tag classes **Immutable tags — one per build.** - `myapp:1.4.2` — the release identity, following semantic versioning so consumers can reason about compatibility. - `myapp:sha-9f3c1ab` — the build identity, tied to the exact commit. Every build gets one, including builds that never become releases. This is what makes "which commit is this container?" answerable without a lookup table. - `myapp:1.5.0-rc.1`, `1.5.0-alpha.4` — pre-releases, still immutable. Enforce immutability at the registry: most registries support a per-repository rule that rejects a push to an existing tag. That converts an accidental repush from a silent artifact swap into a failed pipeline. If the registry cannot enforce it, a CI pre-check (`crane digest` returns non-error → fail) is a workable second best. **Rolling tags — pointers for humans.** - `myapp:1.4` → latest patch on the 1.4 line; `myapp:1` → latest minor on the 1 line; `myapp:latest`, `myapp:stable`. These exist so a developer can `docker run myapp:1.4` without looking anything up, and so dependency bots have something to watch. They are explicitly *forbidden* in Dockerfiles, deployment manifests, and anything a pipeline consumes. ## What deployments reference Digests. CI pushes `1.4.2`, immediately resolves it (`crane digest`), and writes `registry/myapp@sha256:…` into the environment's config repository along with the tag it came from, the commit, and the build URL. Deployment tooling applies that. Two properties follow: - *Every environment's state is a git history.* "What changed in prod between Tuesday and Thursday?" is a diff, with authors and reviews. - *Substitution is impossible.* Nothing between build and run can put different bytes behind the name. Back this with admission policy: reject images referenced by tag, and optionally require a valid signature over the digest. Policy on digests rather than tags is the only enforcement that cannot be defeated by a repush. ## Environment must not be a tag Encoding environments in tags (`:prod`, `:staging`) is attractive because promotion becomes `docker tag`. That is exactly the problem: promotion becomes a *mutation of a shared name*, so at any instant `:prod` means "whatever the last promotion said", nothing has a stable identity, rollback has no target, and two concurrent promotions race. The better model separates *artifact* from *placement*. The artifact is a digest. Placement is a declarative record. If you want environment separation to also carry access-control and retention meaning, promote by **copying the manifest** into a production repository — `crane copy staging/myapp@sha256:… prod/myapp:1.4.2` — asserting the digest is unchanged. Now production has its own credentials, its own retention policy, and possibly its own registry, while the artifact remains provably identical. ## Repository layout One repository per service, namespaced by team or domain: `registry.example.com/payments/ledger-api`. Avoid one repository with tags distinguishing services — it breaks per-service access control, retention, and quota. Keep tags parseable: a fixed, documented grammar makes retention rules ("keep all `sha-*` for 30 days, keep all semver releases for 2 years, keep the last 10 pre-releases") mechanical rather than regex archaeology. ## Retention, and the pin-rot trap Retention interacts with digest pinning: garbage collection removes untagged manifests, and a pinned-but-untagged manifest will eventually disappear. Since every deploy-eligible build keeps a permanent immutable tag, deployed digests stay tagged and therefore retained. Prune aggressively in the `sha-*` space, conservatively in the release space, and keep anything currently referenced by any environment config — which you can compute, because those references are in git. ## Tradeoffs to state out loud - **Discipline cost.** Digest references are unreadable, so tooling must show the tag/commit alongside them everywhere a human looks: dashboards, alerts, deployment records. - **Tag explosion.** A `sha-*` tag per build creates many tags; retention must handle it, and the registry's tag-listing API can get slow with tens of thousands. - **Cross-registry copies cost bandwidth and must carry signatures and attestations**, or production policy checks fail on artifacts that passed in staging. - **Semver requires judgement.** For internal services with a single consumer, `major.minor.patch` is often ceremony; date-based or build-number releases plus commit SHAs can be honest and cheaper. What is not optional is *immutability* — the version grammar is negotiable, the guarantee is not. The defensible summary: immutable tags name artifacts, rolling tags help humans, digests are what run, and environments are declarations in git.

  • Why not just use environment tags like `:prod` and promote with `docker tag`?
    Because promotion then mutates a shared name, so the reference carries no artifact identity: you cannot say which build `:prod` was an hour ago, rollback has no target, and two concurrent promotions race with last-writer-wins. Environment belongs in a declarative record that says "prod runs digest X", which is diffable, reviewable, and auditable.
  • How does this scheme interact with image retention and garbage collection?
    Registries garbage-collect manifests no tag points at, so any digest still referenced by an environment must remain tagged. Because every deploy-eligible build keeps a permanent immutable release tag, deployed digests stay retained. Per-commit `sha-*` tags can be pruned aggressively on a short window, and since all live references live in git you can compute exactly which digests must be exempt.
  • Where do signatures and attestations fit into the naming scheme?
    They attach to the digest, not the tag, and are stored as separate manifests referring to it. That means promotion between registries must copy referrers along, and admission policy should verify the signature over the same digest level it deploys — the index digest for a multi-platform image. Naming and provenance therefore share one anchor: the digest recorded at build time.

Immutable tags are the ISBN of an edition; rolling tags are the shelf label "new releases"; the digest is the physical copy you actually handed the reader.

saying these in an interview costs you the question

  • Encoding the environment in the image tag and promoting by moving it
  • Treating semver tags as immutable without enforcing it in the registry or CI
  • Deploying by tag and assuming the pipeline's pull happens close enough in time to be safe
  • Creating a per-build tag scheme without a retention plan, then hitting registry limits
  • Assuming garbage collection will never remove a manifest that a deployment still references

context