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
latestorproduction. - 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.
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 Step 4 — Deploy by digest so the link cannot drift Jump to heading
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 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.
Related Jump to heading
- Build Provenance & Attestations — the parent topic and the full chain.
- Verifying Attestations Before Deploy — turning the link into a gate.
- Promoting a Release From Staging to Production — where deploying by digest matters most.