Image Tags, Digests, Mutable vs Immutable References, Pull Policies, Pinning, and Reproducibility: Guided Hands-On Workflow and Core Operations
Use a disposable local registry to build two release candidates, publish stable and version aliases, record repository digests, deliberately move a mutable tag, and prove that digest-pinned and floating consumers resolve different immutable content.
Learning objectives
- Create two disposable image revisions from one pinned base identity and prove their local IDs differ.
-
Use a loopback registry to publish immutable version aliases plus
a mutable
stablealias without requiring cloud credentials. -
Capture the digest returned by a push and use
repository@digestto pull/run exact content. -
Move the
stabletag from release A to B and independently verify the behavior of pinned versus floating consumers. - Clean up only resources carrying the Chapter 13 lab identity.
1. Preflight: record the host, context, and build tools
docker version
docker info
docker context show
docker buildx version
docker buildx inspect --bootstrap
Record the client/server/API versions, selected context,
storage/image-store behavior, builder driver, worker platforms,
BuildKit version, and any proxy that affects pulls. The lab
intentionally uses names prefixed with
devops-academy-ch13.
2. Create a bounded synthetic build context
mkdir -p chapter13-lab
cd chapter13-lab
cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
ARG BASE_REF
FROM ${BASE_REF}
ARG RELEASE
LABEL devops-academy.lab="chapter13"
LABEL org.opencontainers.image.version="${RELEASE}"
RUN printf 'release=%s\n' "$RELEASE" > /release.txt
CMD ["sh","-c","cat /release.txt"]
EOF
The release marker is synthetic. No source credentials, private dependencies, or external side effects are involved.
3. Resolve the base tag once, then pin the build input
Resolve the human-friendly BusyBox tag through the registry and capture the top-level digest. This preserves both the readable source identity and the immutable object used by both builds.
BASE_TAG="busybox:1.37.0"
docker buildx imagetools inspect "$BASE_TAG"
BASE_DIGEST=$(
docker buildx imagetools inspect "$BASE_TAG" |
awk '/^Digest:/ {print $2; exit}'
)
test -n "$BASE_DIGEST"
BASE_REF="busybox:1.37.0@$BASE_DIGEST"
printf 'BASE_REF=%s\n' "$BASE_REF"
For a multi-platform base this may be an index digest. BuildKit still selects the appropriate platform descriptor while the parent index identity remains fixed.
4. Build release A and B from the same pinned base
docker build --pull \
--build-arg BASE_REF="$BASE_REF" \
--build-arg RELEASE="A" \
--label devops-academy.lab=chapter13 \
-t devops-academy-ch13-a:local .
docker build --pull \
--build-arg BASE_REF="$BASE_REF" \
--build-arg RELEASE="B" \
--label devops-academy.lab=chapter13 \
-t devops-academy-ch13-b:local .
docker image inspect devops-academy-ch13-a:local \
--format 'A ID={{.Id}} version={{index .Config.Labels "org.opencontainers.image.version"}}'
docker image inspect devops-academy-ch13-b:local \
--format 'B ID={{.Id}} version={{index .Config.Labels "org.opencontainers.image.version"}}'
The builds differ only in declared release input. Their local image IDs should differ. That is local evidence; registry digests come next.
5. Start an exact disposable loopback registry
REGISTRY_TAG="registry:2"
REGISTRY_DIGEST=$(
docker buildx imagetools inspect "$REGISTRY_TAG" |
awk '/^Digest:/ {print $2; exit}'
)
test -n "$REGISTRY_DIGEST"
REGISTRY_REF="registry:2@$REGISTRY_DIGEST"
docker run -d \
--name devops-academy-ch13-registry \
--label devops-academy.lab=chapter13 \
-p 127.0.0.1:5000:5000 \
"$REGISTRY_REF"
curl -fsS http://127.0.0.1:5000/v2/
Loopback publishing prevents external exposure. If your platform requires additional daemon policy even for loopback HTTP, stop here and use the simulation path below; do not disable TLS or broaden registry trust for a course exercise.
6. Publish immutable release A and point stable at it
REPO="localhost:5000/devops-academy/ch13"
docker tag devops-academy-ch13-a:local "$REPO:release-a"
docker tag devops-academy-ch13-a:local "$REPO:stable"
docker push "$REPO:release-a"
docker push "$REPO:stable" | tee push-stable-a.txt
DIGEST_A=$(
awk '/digest:/ {
for (i=1; i<=NF; i++) if ($i=="digest:") {print $(i+1); exit}
}' push-stable-a.txt
)
test -n "$DIGEST_A"
printf 'DIGEST_A=%s\n' "$DIGEST_A"
The returned digest is the immutable registry identity for the
pushed manifest or index. Preserve push-stable-a.txt as
evidence.
7. Pull and run A by digest
docker pull "$REPO@$DIGEST_A"
docker run --rm \
--name devops-academy-ch13-pinned-a \
"$REPO@$DIGEST_A"
docker image inspect "$REPO@$DIGEST_A" \
--format 'ID={{.Id}} RepoDigests={{json .RepoDigests}}'
The output should report release=A. The reference
contains no mutable tag, so later movement of
stable cannot change what
$DIGEST_A identifies.
8. Move the mutable stable alias to B
docker tag devops-academy-ch13-b:local "$REPO:release-b"
docker tag devops-academy-ch13-b:local "$REPO:stable"
docker push "$REPO:release-b"
docker push "$REPO:stable" | tee push-stable-b.txt
DIGEST_B=$(
awk '/digest:/ {
for (i=1; i<=NF; i++) if ($i=="digest:") {print $(i+1); exit}
}' push-stable-b.txt
)
test -n "$DIGEST_B"
test "$DIGEST_A" != "$DIGEST_B"
printf 'A=%s\nB=%s\n' "$DIGEST_A" "$DIGEST_B"
The tag text remained stable; its resolved digest
changed. That is the behavior the chapter is designed to make
visible.
9. Compare floating and pinned consumers
docker run --rm --pull=always \
--name devops-academy-ch13-floating \
"$REPO:stable"
docker run --rm --pull=always \
--name devops-academy-ch13-still-a \
"$REPO@$DIGEST_A"
The floating consumer should print release=B. The
digest-pinned consumer should still print release=A.
The registry is not “remembering a version tag”; it is resolving
either a mutable alias or an immutable digest.
10. Preserve container/image correlation when containers are retained
docker create --name devops-academy-ch13-proof-a "$REPO@$DIGEST_A"
docker create --name devops-academy-ch13-proof-b "$REPO:stable"
docker inspect devops-academy-ch13-proof-a devops-academy-ch13-proof-b \
--format 'name={{.Name}} requested={{.Config.Image}} localImageID={{.Image}}'
.Config.Image records the requested reference while
.Image correlates the container to a local image
object. Preserve both during incident response.
11. No-registry simulation path
If loopback registry publication is unavailable, perform the conceptual alias move locally. This proves tag mutability and image-ID retention but does not produce a repository digest. State that limitation explicitly.
docker tag devops-academy-ch13-a:local devops-academy-ch13:stable
A_ID=$(docker image inspect devops-academy-ch13:stable --format '{{.Id}}')
docker tag devops-academy-ch13-b:local devops-academy-ch13:stable
B_ID=$(docker image inspect devops-academy-ch13:stable --format '{{.Id}}')
printf 'A_ID=%s\nB_ID=%s\n' "$A_ID" "$B_ID"
12. Exact cleanup
docker rm devops-academy-ch13-proof-a devops-academy-ch13-proof-b 2>/dev/null || true
docker rm -f devops-academy-ch13-registry 2>/dev/null || true
docker image rm \
"$REPO:stable" "$REPO:release-a" "$REPO:release-b" \
devops-academy-ch13-a:local devops-academy-ch13-b:local \
2>/dev/null || true
Do not run broad image/system/volume prune commands. These exact names are the lab boundary.
13. Small challenge
Without running a command sequence from memory, explain which single
reference you would hand to a production deployment if release A has
passed approval but the stable tag has already moved to
B. The answer must preserve repository identity and immutable
digest, and your reasoning must identify why a local image ID alone
is insufficient.
Knowledge check
After stable moves from A to B, what does
repo@DIGEST_A mean?
It still names immutable release A as long as that content remains available in the registry.
Why capture the digest printed by
docker push?
It is registry-side evidence tying the pushed reference to immutable manifest/index content.
Why can the no-registry simulation not prove a RepoDigest?
Because no registry accepted or served the manifest; it only demonstrates local tag-to-image-ID aliasing.
What is unsafe about changing daemon registry security just to finish this lab?
It expands the host trust boundary for a disposable exercise. The course requires using the simulation path instead.
If the floating run prints B and the pinned run prints A, what has been demonstrated?
That tag resolution changed while digest identity remained stable.
Official references and version notes
-
docker image ls— digest display, local image IDs, and digest-addressed pull/create/run examples. -
docker image inspect— low-level local image metadata, including platform-aware inspection on capable stores. -
docker run— current--pull=missing|always|neversemantics. -
Compose
pull_policy—always,never,missing,build,daily,weekly, andevery_<duration>. - Docker build best practices — pinning base images by digest, controlled updates, and auditability tradeoffs.
-
docker buildx imagetools inspect— registry-side manifest/index and platform inspection. - Image digests — manifest-list/index versus per-platform image digest distinctions.
- OCI Image Index Specification — immutable index descriptors and platform selection.
- OCI Image Manifest Specification — config and layer descriptors addressed by digest.
- Docker Engine 29 release notes — Engine 29.8.1 baseline used when authoring this chapter.
- Buildx releases and BuildKit releases — current build-tool release history.
Version-sensitive statements were rechecked against primary documentation on 2026-09-21. The authoring baseline is Docker Engine 29.8.1, Buildx 0.37.1, BuildKit 0.33.0, and Dockerfile frontend 1.27.0, but every executable lab records the versions and context actually present. Registry tags, platform indexes, local image stores, and Compose behavior can evolve independently; prefer observed digests and current primary documentation over memorized output.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.