Chapter 13Lesson 02~120 minutes

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.

Local registryRepoDigestsPinned pullMutable aliasVerification

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 stable alias without requiring cloud credentials.
  • Capture the digest returned by a push and use repository@digest to pull/run exact content.
  • Move the stable tag 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.
Disposable-lab boundary. Use only a local loopback registry and synthetic content. If your environment refuses loopback HTTP registry traffic, use the “no-registry simulation” subsection instead of weakening TLS, adding broad insecure registries, or changing a production daemon.

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.

Next lesson

Next: Configuration, Design Choices, and Tradeoffs

Turn the mechanics into release-policy choices for development, CI, staging, and production.

Knowledge check

After stable moves from A to B, what does repo@DIGEST_A mean?

Why capture the digest printed by docker push?

Why can the no-registry simulation not prove a RepoDigest?

What is unsafe about changing daemon registry security just to finish this lab?

If the floating run prints B and the pinned run prints A, what has been demonstrated?

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.