A Tagging Strategy for Releases
A tag is a movable pointer — Chapter 2 topic 09 established that, and a release process has to be built around it rather than in spite of it. The discipline that works is three kinds of tag living on the same image at once: an immutable semver release tag like driftwood/web:1.4.0 that is pushed exactly once, moving convenience tags like 1.4, 1, and latest that track the newest matching release, and a commit-SHA tag like driftwood/web:git-9f3c2a1 for exact build traceability.
1.4.0 — cut once and never re-pushed, so it always means the same bytes.1.4, 1, latest — readable shortcuts that advance to the newest matching release, never to be pinned in production.@sha256:… — names exact bytes; this is what the deploy actually pins so a moved tag can never change what runs.The rule underneath all three: you deploy by digest, and the tags are for humans and tooling to reason about. A digest pins exact bytes; the tags are the readable handles that make a registry navigable. Confusing the two — treating a moving tag as if it were a version — is the source of every tagging incident in this topic.
Immutable Release Tags
driftwood/web:1.4.0 is cut once per release and never re-pushed. Anyone who pins it gets the same bytes forever, and that permanence is the entire contract that makes 1.4.0 a meaningful version rather than a guess about what was running. This is the direct application of the movable-pointer fact from Chapter 2: a tag can move, so the discipline is choosing which ones never will.
Moving Convenience Tags
1.4 follows the latest 1.4.x patch, 1 the latest 1.x, and latest the newest release overall. These are deliberately mutable: a docker pull driftwood/web:1.4 is supposed to pick up the newest patch, which is convenient for a human poking at the registry and exactly wrong for anything production pins to. They are readable shortcuts, not version locks.
Commit-SHA Tags for Traceability
Tagging each build git-<sha> ties the image back to the exact source commit in driftwood-io/app. When an image misbehaves, that tag answers "which commit is this" without trusting a human-assigned version number that someone may have bumped wrong. Semver says what the team intended to ship; the SHA says what actually went in.
$ IMG=registry.driftwood.example/driftwood/web $ docker tag driftwood/web:build $IMG:1.4.0 # immutable, pushed once $ docker tag driftwood/web:build $IMG:1.4 # moving: latest 1.4.x $ docker tag driftwood/web:build $IMG:latest # moving: newest release $ docker tag driftwood/web:build $IMG:git-9f3c2a1 # exact source commit $ docker push --all-tags $IMG
All four names point at the same bytes the moment they are pushed. What differs is the contract: 1.4.0 and git-9f3c2a1 will always mean these bytes, while 1.4 and latest are free to advance to the next release.
Never Re-Push a Release Tag
Re-pushing 1.4.0 to "fix" something silently changes the bytes under a name other people have pinned, breaking the immutability contract that made the tag worth having. The fix for a bug in 1.4.0 is to cut 1.4.1 and let 1.4 and latest move forward to it. A rollback then means "run 1.4.0 again" and actually gets you the same code — which is the only reason rollback-by-version works at all.
Deploy by Digest, Tag for Readability
CI resolves the pushed tag to its @sha256:… digest and the deploy pins that digest, so a moved tag can never change what production runs. The tags stay readable for people; the digest is what the orchestrator actually pulls. This closes the loop with Chapter 2 topic 09: tags are how humans navigate, digests are how machines guarantee, and a release process uses both for the jobs each is good at.
Recording that resolved digest in the release log is the last piece. After an incident, "what shipped as 1.4" has an exact answer — a specific @sha256:… — even though the 1.4 tag has since advanced to a later patch. Without the recorded digest, the moving tag has erased the evidence.
- Re-pushing
driftwood/web:1.4.0after a hotfix instead of cutting1.4.1— every consumer who pinned1.4.0now silently gets different code under the same version, and a rollback to "1.4.0" no longer means anything specific. - Running
driftwood/web:latestin production as if it pins a version —latestis just the default tag and moves on every release, so an autoscale event or node replacement can pull newer code than the rest of the fleet. - Tagging only by semver and never by commit SHA — when an image misbehaves there is no precise link back to the source commit, only a version number a human chose and may have gotten wrong.
- Deploying by the moving
1.4tag and never recording the resolved digest — after an incident there is no exact record of which bytes ran, because the tag has since advanced to a later patch.
- Push the immutable
driftwood/web:1.4.0exactly once per release and treat re-pushing a version tag as a process error; ship1.4.1for any change, however small. - Move the convenience tags (
1.4,1,latest) forward to each new release so humans get a readable "latest patch" handle without depending on it for reproducibility. - Tag every build with its commit SHA (
git-<sha>) so any running image traces back to an exact commit indriftwood-io/app. - Resolve each release tag to its digest in CI and deploy by
@sha256:…, recording that digest in the release log so "what shipped" is always answerable exactly.
Knowledge Check
Which tags should be immutable and which should move, and why?
- The semver release tag and the commit-SHA tag are immutable;
1.4,1, andlatestare meant to move to the newest matching release - Only
latestis immutable; the semver and the commit-SHA tags are both re-pointed and updated on every single push - All tags become read-only automatically the moment they are first pushed, so the immutable-versus-moving distinction does not matter
- Tags containing a commit SHA are inherently mutable while version tags are not, because the SHA portion changes on each rebuild
Why does re-pushing a release tag like 1.4.0 to fix a bug break consumers?
- Everyone who pinned
1.4.0now gets different bytes under the same name, so the version no longer identifies a fixed artifact - It corrupts the registry's blob store because two distinct images are never allowed to share a single tag name
- It forces a full re-upload of every layer in the image, which is both slow and expensive on the consumers' side
- It automatically deletes the
1.4andlatesttags that previously pointed at the same nearby bytes
What does a commit-SHA tag add over a semver tag alone?
- A precise link back to the exact source commit, independent of a human-assigned version that could be wrong
- It replaces the image content digest outright, so you no longer need to record a
@sha256:…for the release - It makes the image pull faster because commit SHAs index more efficiently in the registry than semver version strings do
- It is automatically immutable at the registry level in a way that ordinary semver tags can never be
Why do production deploys pin a digest while the tags stay readable?
- A digest names exact bytes so a moved tag can never change what runs, while tags stay as readable handles for humans
- Pulling by digest is meaningfully faster than pulling by tag, so production always prefers it for latency reasons
- A digest hides which underlying image is actually running, which is considered more secure for production workloads
- Registries refuse to serve images marked for production by tag, so pinning a digest is strictly required to deploy them
You got correct