Chapter 10Lesson 05~135 minutes

Checkpoint Lab — Multi-Stage Builds, Minimal Runtime Images, Distroless Patterns, Debug Stages, and Build Separation

The checkpoint produces build, debug, and minimal runtime targets from one Dockerfile, compares their contents, size, user, entrypoint, and identities, extracts the release binary without starting the container, and performs diagnosis through the debug target rather than mutating the runtime image. The result is an evidence-backed build/runtime separation dossier ready for Chapter 11 cache work.

Checkpoint labBuild/debug/runtime targetsDigest evidenceExact cleanupChapter 11 bridge

Learning objectives

  • Create one reproducible multi-stage Dockerfile with build, debug, and scratch runtime targets and a clearly bounded artifact crossing.
  • Predict target-specific contents, user, shell availability, size direction, and output state before building, then verify every prediction.
  • Capture source hashes, builder/frontend assumptions, base references, target metadata, image IDs/digests, binary hashes, runtime output, and debug evidence.
  • Demonstrate a debugging workflow through the dedicated debug target instead of mutating the immutable runtime container.
  • Clean up only chapter-owned containers/images/files and bridge the target/output evidence model to Chapter 11 cache semantics.
Chapter 10 evidence baseline — verified 2026-09-21. Mandatory exercises use only a synthetic Go source file, public base images, local Docker/Buildx, exact chapter-owned tags/containers, and no real credentials, production daemons, registries, or privileged host access. At verification time Docker Engine 29.8.1, Buildx 0.37.1, and BuildKit 0.33.0 (built-in Dockerfile frontend 1.27.0) are the current course baselines, but every lab records the actual installed versions and builder state. Docker documents named multi-stage builds, COPY --from, and --target as current core semantics; scratch remains the empty reserved base. Current Distroless Debian 13 images intentionally omit ordinary shells/package managers and publish separate nonroot/debug/debug-nonroot variants. Base-image tags are readable selectors, not immutable release identity: record or substitute reviewed digests when reproducibility matters.

1. Checkpoint mission

Create one small application and one Dockerfile that produce three operationally different targets: build (toolchain and artifact creation), debug (same artifact plus shell/tools), and runtime (scratch plus only the artifact). The checkpoint is complete only when you can prove the artifact boundary and target-specific runtime state with evidence—not merely when all three builds are green.

2. Preflight and assumptions

mkdir -p ch10-checkpoint/evidence
cd ch10-checkpoint

docker version | tee evidence/00-docker-version.txt
docker context show | tee evidence/01-context.txt
docker buildx version | tee evidence/02-buildx-version.txt
docker buildx inspect --bootstrap | tee evidence/03-builder.txt

docker buildx imagetools inspect golang:1.26 \
  | tee evidence/04-golang-base.txt
docker buildx imagetools inspect busybox:1.37.0 \
  | tee evidence/05-debug-base.txt

Record host architecture and builder platforms. If your environment cannot pull these public images, use reviewed equivalents already available in your authorized environment and document the substitution; do not use a production registry credential just for the lab.

3. Write predictions before building

Prediction A: build target contains compiler/toolchain and is not the release.
Prediction B: debug target contains /app plus a shell and runs as UID 65532.
Prediction C: runtime target contains /app, runs as UID 65532, and has no /bin/sh.
Prediction D: debug and runtime contain byte-identical /app for the same build inputs.
Prediction E: runtime is smaller than the toolchain-heavy build/single target.
Prediction F: each target has distinct image/config identity even when /app matches.

Save these as evidence/06-predictions.txt. Predictions make the checkpoint falsifiable.

4. Create source and Dockerfile

package main

import (
    "fmt"
    "os"
    "runtime"
)

func main() {
    fmt.Printf("ch10-checkpoint uid=%d gid=%d %s/%s\\n",
        os.Getuid(), os.Getgid(), runtime.GOOS, runtime.GOARCH)
}
# syntax=docker/dockerfile:1
ARG GO_BASE=golang:1.26
ARG DEBUG_BASE=busybox:1.37.0

FROM ${GO_BASE} AS build
WORKDIR /src
COPY main.go .
ARG TARGETOS
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
    go build -trimpath -ldflags='-s -w -buildid=' -o /out/app ./main.go
RUN sha256sum /out/app > /out/app.sha256

FROM ${DEBUG_BASE} AS debug
COPY --from=build /out/app /app
USER 65532:65532
ENTRYPOINT ["/app"]

FROM scratch AS runtime
COPY --from=build --chown=65532:65532 /out/app /app
USER 65532:65532
ENTRYPOINT ["/app"]
sha256sum main.go Dockerfile | tee evidence/07-source-hashes.txt

5. Build-stage evidence without promoting it as release

Build the build target only when you need to inspect its filesystem/toolchain. Load it under a clearly non-release tag:

docker buildx build \
  --target build --load --progress=plain \
  --metadata-file evidence/08-build-target-metadata.json \
  --label devops-academy.lab=ch10 \
  -t devops-academy-ch10:build \
  . 2>&1 | tee evidence/08-build-target.log

docker image inspect devops-academy-ch10:build \
  --format 'ID={{.Id}} Size={{.Size}} User={{.Config.User}}' \
  | tee evidence/09-build-target-inspect.txt

This image exists only to make build-state evidence observable. Do not publish it as the application release.

6. Build and inspect the debug target

docker buildx build \
  --target debug --load --progress=plain \
  --metadata-file evidence/10-debug-metadata.json \
  --label devops-academy.lab=ch10 \
  -t devops-academy-ch10:debug \
  . 2>&1 | tee evidence/10-debug.log

docker run --rm devops-academy-ch10:debug \
  | tee evidence/11-debug-runtime.txt

docker run --rm --entrypoint /bin/sh devops-academy-ch10:debug \
  -c 'printf "uid=%s gid=%s\\n" "$(id -u)" "$(id -g)"; sha256sum /app; ls -ln /app' \
  | tee evidence/12-debug-evidence.txt

7. Build and inspect the minimal runtime target

docker buildx build \
  --target runtime --load --progress=plain \
  --metadata-file evidence/13-runtime-metadata.json \
  --label devops-academy.lab=ch10 \
  -t devops-academy-ch10:runtime \
  . 2>&1 | tee evidence/13-runtime.log

docker image inspect devops-academy-ch10:runtime \
  --format 'ID={{.Id}} Size={{.Size}} User={{.Config.User}} Entrypoint={{json .Config.Entrypoint}}' \
  | tee evidence/14-runtime-inspect.txt

docker run --rm devops-academy-ch10:runtime \
  | tee evidence/15-runtime-output.txt

8. Prove the same artifact crossed into both targets

DEBUG_CID="devops-academy-ch10-debug-evidence"
RUNTIME_CID="devops-academy-ch10-runtime-evidence"

docker create --name "$DEBUG_CID" devops-academy-ch10:debug \
  > evidence/16-debug-container-id.txt
docker create --name "$RUNTIME_CID" devops-academy-ch10:runtime \
  > evidence/16-runtime-container-id.txt

docker cp "$DEBUG_CID":/app ./evidence/debug-app
docker cp "$RUNTIME_CID":/app ./evidence/runtime-app
sha256sum ./evidence/debug-app ./evidence/runtime-app \
  | tee evidence/17-artifact-hashes.txt
cmp ./evidence/debug-app ./evidence/runtime-app \
  && echo 'artifact-bytes=identical' \
  | tee evidence/18-artifact-compare.txt

docker rm "$DEBUG_CID" "$RUNTIME_CID" \
  | tee evidence/19-evidence-containers-cleanup.txt

Image identities should differ because the surrounding filesystems/config differ. The application bytes should match because both targets copy from the same build-stage path in the same build definition and unchanged inputs.

9. Demonstrate debugging without mutating runtime

set +e
docker run --rm --entrypoint /bin/sh devops-academy-ch10:runtime \
  > evidence/20-runtime-shell.stdout \
  2> evidence/20-runtime-shell.stderr
printf 'runtime-shell-exit=%s\n' "$?" \
  | tee evidence/20-runtime-shell-exit.txt
set -e

docker run --rm --entrypoint /bin/sh devops-academy-ch10:debug \
  -c 'echo "diagnostic target"; id; ls -l /app' \
  | tee evidence/21-debug-session.txt

The first command should fail because scratch has no shell. The second should succeed because the debug target intentionally supplies one. Neither command changes the release image.

10. Compare target configuration and size

for image in \
  devops-academy-ch10:build \
  devops-academy-ch10:debug \
  devops-academy-ch10:runtime
do
  docker image inspect "$image" \
    --format '{{join .RepoTags ","}} id={{.Id}} size={{.Size}} user={{.Config.User}} entrypoint={{json .Config.Entrypoint}}'
done | tee evidence/22-target-comparison.txt

docker image history --no-trunc devops-academy-ch10:runtime \
  | tee evidence/23-runtime-history.txt

Record whether Prediction E holds in your architecture/environment. If it does not, investigate rather than altering the observation.

11. Evidence packet checklist

Evidence Question answered
Docker/Buildx/builder/frontend versions Which build engine executed the graph?
Base imagetools output Which external base identities were resolved?
Source/Dockerfile hashes Which declared inputs were used?
Plain build logs + metadata JSON Which target/output completed and what result identity was reported?
Target image inspect/history What size/user/entrypoint/local identity does each target have?
Extracted application hashes Did the same artifact cross into debug and runtime?
Runtime output + shell failure Does the release run non-root and remain shell-less?
Debug session Can the artifact be diagnosed through a separate image identity?

12. Exact cleanup and rollback

docker image rm \
  devops-academy-ch10:build \
  devops-academy-ch10:debug \
  devops-academy-ch10:runtime \
  | tee evidence/24-image-cleanup.txt

docker image ls --filter label=devops-academy.lab=ch10 \
  | tee evidence/25-final-image-check.txt

date -u +%Y-%m-%dT%H:%M:%SZ \
  | tee evidence/26-finished-at.txt

Do not use whole-system or whole-builder prune operations. The checkpoint created exact image tags and exact temporary container names, so exact cleanup is sufficient. Preserve the evidence directory if you want to review the checkpoint.

13. Prediction review

Create evidence/27-prediction-results.txt and mark each prediction confirmed, rejected, or qualified. Include the observed image sizes, user configuration, shell result, artifact hashes, and any platform-specific limitation. A failed prediction with preserved evidence is more valuable than editing the prediction after the fact.

14. Operational review and Chapter 11 handoff

Chapter 10 adds a critical boundary to the Docker evidence chain: rich build state and minimal runtime state are separate deliverables connected by explicit artifact copies. You can now prove that the compiler did not become the release, that debug tooling has a different image identity, that the runtime launches non-root, and that the same application bytes crossed into debug and release targets.

Chapter 11 turns to Build Cache, Cache Mounts, Bind Mounts, Secret Mounts, SSH Mounts, and Cache Import/Export. The next challenge is to make these multi-stage builds fast without allowing cache or ephemeral build inputs to become untracked release identity or secret leakage.

Next chapter

Next: Build Cache, Cache Mounts, Bind Mounts, Secret Mounts, SSH Mounts, and Cache Import/Export

Keep the artifact boundary intact while learning exactly which build state may be reused and which sensitive inputs must remain ephemeral.

Knowledge check

Which evidence proves debug and runtime contain the same application bytes?

Why should build and runtime image identities differ?

What is the correct interpretation of the scratch shell failure?

Why preserve metadata files as well as local image IDs?

What is the Chapter 11 risk introduced by optimization?

Official references and version notes

Version and compatibility note

Version-sensitive statements were rechecked against primary sources on 2026-09-21. Docker Engine 29.8.1 is the current Engine 29 patch baseline (released 2026-09-15); Buildx 0.37.1 is the current upstream Buildx release (2026-09-11); BuildKit 0.33.0 is the current upstream BuildKit release (2026-09-02) and ships built-in Dockerfile frontend 1.27.0. Current Distroless documentation lists Debian 13 families with latest, nonroot, debug, and debug-nonroot variants and warns that images intentionally lack a shell. Always record the actual local versions, builder/frontend, base-image digests, target platform, and external-image identity because installation bundles and image tags evolve independently.

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.