← Docker articles

ACE-U field notes

Tags, digests and promotion: how an image should travel from commit to production

Stop deploying latest. Tag by commit, deploy by digest, and promote the exact artifact that passed your tests.

Shubham Anandani··4 min read

Problem

Why is staging running something different from production? If the answer involves the word latest, you have found the bug. A tag is a movable pointer, so two environments can point at different content under the same name, and nobody can say which one is right. Release hygiene comes down to one idea: the artifact that passed your tests is exactly the artifact that runs later.

Options and trade-offs

  • Mutable tags such as latest or a branch name: convenient, and impossible to trace.
  • Commit-based tags: readable and traceable, but a tag can still be moved.
  • Digests: the content address of the image, which cannot change. Deploying by digest guarantees what runs.

My recommendation

Tag every build with the commit it came from, record the digest, deploy by digest, and promote by copying the existing image rather than rebuilding. Keep a small catalog of what was released, so a rollback is just promoting an older entry.

Worked example

A release script that builds a multi-platform image, captures its digest, records it, and promotes the same digest to staging:

#!/usr/bin/env bash
set -euo pipefail

REGISTRY=registry.example.com/team/app
SHA=$(git rev-parse --short=12 HEAD)

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t "$REGISTRY:$SHA" \
  --metadata-file meta.json \
  --push .

DIGEST=$(jq -r '."containerimage.digest"' meta.json)
echo "$SHA $DIGEST $(date -u +%FT%TZ)" >> release-catalog.txt

# Promote: same content, new name. No rebuild.
docker buildx imagetools create --tag "$REGISTRY:staging" "$REGISTRY@$DIGEST"

# Deploy by digest, not tag:
echo "image: $REGISTRY@$DIGEST"

To make a running service tell you what it is, stamp the commit into the jar. In Gradle:

tasks.jar {
    manifest {
        attributes(
            "Implementation-Version" to providers.environmentVariable("GIT_SHA").orElse("dev")
        )
    }
}
public class Version {
    public static String current() {
        String v = Version.class.getPackage().getImplementationVersion();
        return v != null ? v : "dev";
    }
}

Expose Version.current() on a diagnostics route, and the answer to which build is this becomes one request.

Checklist

  1. Tag every image with the short commit hash, and never reuse a tag.
  2. Record commit, digest and time in a catalog.
  3. Deploy by digest.
  4. Promote by copying the image, not by rebuilding it.
  5. Check that the promoted manifest still contains every platform you need.
  6. Protect release images from registry clean-up so you can still debug and roll back.

Where this doesn't apply

For throwaway local images, a plain tag is fine. The discipline pays off from the first shared environment onward.

One honest limitation

Digest pinning needs tooling support, a catalog, and registry permissions to copy images. It is more work than a tag, and teams that skip the catalog lose most of the benefit.

Sources

  • Docker Docs: Image digests
  • Docker Docs: buildx imagetools create
  • Docker Docs: Build metadata file