Linking a container image to its commit Jump to heading

During an incident, the gap between “this image is misbehaving” and “this is the change that caused it” is frequently twenty minutes of asking people. The link exists — some pipeline built that image from some commit — and it is usually recorded nowhere durable. Three cheap mechanisms make the round trip a command in each direction, and having all three means the answer survives one of them being unavailable. This recipe sets them up, within build provenance and attestations.

When to use this approach Jump to heading

  • Nobody can state quickly which commit a running artefact came from.
  • Deployments are identified by a mutable tag such as latest or production.
  • An incident review asked for the commit and it took an investigation.
  • You have provenance attestations and want a fallback when the store is unreachable.
  • If your deployment already records the commit and everyone knows where, you have this covered.

Step 1 — Stamp the commit into the image itself Jump to heading

Labels travel with the image and need no external system to read.

ARG GIT_COMMIT
ARG GIT_TAG
LABEL org.opencontainers.image.revision="$GIT_COMMIT"
LABEL org.opencontainers.image.version="$GIT_TAG"
LABEL org.opencontainers.image.source="https://github.com/acme/app"
docker build \
  --build-arg GIT_COMMIT="$(git rev-parse HEAD)" \
  --build-arg GIT_TAG="$(git describe --tags --always)" \
  -t ghcr.io/acme/app:"$(git rev-parse --short HEAD)" .
# Verification: the label is readable from the image, without the registry API
docker inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' \
  ghcr.io/acme/app:"$(git rev-parse --short HEAD)"

The standard label names matter: tooling, scanners and registries all understand org.opencontainers.image.revision, so using the conventional key means things you have not configured will read it correctly.

Three independent links, in case one is unavailableAn image label travels inside the artefact and needs nothing external. A tag on the commit records the digest in the repository. A provenance attestation is signed and verifiable. Any one answers the question; having all three means an unreachable system is an inconvenience rather than a blocker.Image labelinside the artefactno external lookupunsignedGit tag messagein the repositorysurvives the registryunsigned or signedAttestationsigned, verifiableneeds the storeauthoritativeonly the third is proof; the first two are what you use when the third is unreachable

Step 2 — Record the digest against the commit, in the repository Jump to heading

The repository is the one system that will still be there.

digest=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/acme/app:"$SHORT")
git tag -a "build/$(git rev-parse --short HEAD)" -m "image: $digest"
git push origin "build/$(git rev-parse --short HEAD)"
# From a commit to its image
git tag -l "build/$(git rev-parse --short HEAD)" --format='%(contents)'
# Verification: the tag resolves and names a digest
git tag -l 'build/*' --sort=-creatordate --format='%(refname:short) %(contents:subject)' | head -3

Step 3 — Make the reverse lookup work Jump to heading

From a digest back to a commit is the direction you need during an incident.

# From the running deployment
image=$(kubectl get deploy app -n production -o jsonpath='{.spec.template.spec.containers[0].image}')
echo "$image"
# Route 1: the label, straight from the registry
crane config "$image" | jq -r '.config.Labels["org.opencontainers.image.revision"]'
# Route 2: the attestation, which is the signed answer
gh attestation verify "oci://$image" --owner acme --format json \
  | jq -r '.[0].verificationResult.statement.predicate.buildDefinition
           .resolvedDependencies[0].digest.gitCommit'
# Verification: both routes agree
The incident question, answered in three commandsRead the digest from the deployment, read the commit from the image label or attestation, and read the change from the repository. Each step is a command against a system that is already available during an incident.responderdeploymentregistryrepositorywhat image is running?app@sha256:4f2a…read the revision labelcommit a1b2c3git log -1 a1b2c3author, date, subjectthree commands replace the conversation that usually happens here

A mutable tag breaks every link the moment it is repointed.

# Not this: the tag may point somewhere else tomorrow
kubectl set image deploy/app app=ghcr.io/acme/app:production -n production
# This: the digest is the artefact
kubectl set image deploy/app app=ghcr.io/acme/app@"$digest" -n production
# Verification: what is running is a digest, not a tag
kubectl get deploy app -n production -o jsonpath='{.spec.template.spec.containers[0].image}' | grep -q '@sha256:' \
  && echo "pinned by digest"

SAFETY WARNING — an image referenced by a mutable tag can be replaced in the registry without anything in the cluster changing, so a verified deployment can silently become an unverified one. Any provenance verification performed against a tag verifies whatever that tag pointed at during the check. Resolve to a digest, verify the digest, and deploy the digest.

Step 5 — Keep a ledger that outlives the systems Jump to heading

Registries are pruned, clusters are rebuilt, and attestation stores have retention policies.

# One append-only line per deployment, in the repository
printf '%s\t%s\t%s\t%s\n' \
  "$(date -u +%FT%TZ)" production "$(git rev-parse HEAD)" "$digest" >> deployments.tsv
git add deployments.tsv && git commit -m 'chore(deploy): record the production deployment'
# What was running on a given date?
awk -F'\t' '$1 < "2026-09-10" && $2 == "production"' deployments.tsv | tail -1
# Verification: the ledger answers a question the registry no longer can
grep -c production deployments.tsv
Deploying by tag against deploying by digestA tag is a pointer the registry can repoint at any time, so what is running may not be what was verified. A digest names content, so the artefact that was verified is exactly the artefact that runs and the link back to its commit cannot drift.Deploy by tagDeploy by digestwhat runswhatever the tag points atthe verified artefactlink to a commitmay driftfixedverification meaningfulat check time onlyyesreadabilityhuman-friendlya hashkeep the tag for people and deploy the digest for machines

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Are labels enough on their own? Jump to heading

For operations, frequently yes; for security, no. A label is unsigned metadata that anyone who can push an image can set to any value, so it answers “what does this image claim” rather than “what is true”. Use labels for speed and attestations for proof, and check that they agree.

What if the image was built from a dirty working tree? Jump to heading

Then the recorded commit is misleading, and the build should say so. Appending a marker — a1b2c3-dirty, from git describe --dirty — makes that visible rather than silently wrong. Release builds should refuse to run from a dirty tree at all.

How do we handle multi-architecture images? Jump to heading

The manifest list has its own digest and each platform image has another. Attest and record the manifest list digest, which is what deployments reference, and the per-platform links follow from it. Verifying a single-platform digest while deploying the list is a common and confusing mismatch.