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.
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.
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.
Knowledge check
Which evidence proves debug and runtime contain the same application bytes?
The extracted files' matching SHA-256 values and byte-for-byte
cmp result.
Why should build and runtime image identities differ?
They have different filesystems/configurations and responsibilities even though a selected artifact crosses between them.
What is the correct interpretation of the scratch shell failure?
It confirms the runtime does not include /bin/sh;
it does not mean the application is unhealthy if the application
itself runs successfully.
Why preserve metadata files as well as local image IDs?
Build-result digest/metadata and local image configuration identity are related but distinct evidence surfaces.
What is the Chapter 11 risk introduced by optimization?
Cache and ephemeral build inputs can improve speed but must not become untrusted release identity, leak secrets, or erase the ability to explain which inputs produced the artifact.
Official references and version notes
-
Docker multi-stage builds
— named stages,
COPY --from, stage reuse, and stopping at a specific target. -
Dockerfile reference
—
FROM, stage naming,COPY --from,USER,ENTRYPOINT, and current frontend semantics. -
Base images
— choosing base images and creating minimal images with the
reserved
scratchbase. - Docker build best practices — small trusted bases, rebuilding, pinning, decoupling applications, and non-root considerations.
-
docker buildx build—--target,--metadata-file, progress, output, and build-result evidence. -
docker image inspect— size and runtime configuration evidence. -
docker image history— image-history evidence and its limits. - GoogleContainerTools/distroless — current Distroless image families, Debian 13 tags, nonroot/debug variants, no-shell behavior, and signature guidance.
- Distroless base image contents — static/base/base-nossl runtime contents such as CA certificates, tzdata, glibc, and libssl.
- Distroless support policy — current Debian-family support timelines.
- Docker Engine 29 release notes — current Engine baseline and bundled component updates.
- Buildx releases — current Buildx release history.
- BuildKit releases — current BuildKit and built-in Dockerfile frontend history.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.