Chapter 40Lesson 05~245 minutes

Checkpoint Lab — Docker Performance Engineering: Build Speed, Image Size, Runtime Overhead, Network, Storage, and Resource Profiling

Produce a before/after Docker performance packet, isolate one bottleneck, change one Docker-specific variable, re-run the same benchmark, and state the statistical and platform limits of the result.

CheckpointBaselineOne-variable changeRe-measureEvidence packet

Learning objectives

  • Capture a reproducible baseline with exact Docker/builder/base/source identity and repeated measurements.
  • Identify one dominant bottleneck from evidence instead of choosing an optimization first.
  • Apply exactly one Docker-specific change and predict which state should change.
  • Re-run the same benchmark and preserve before/after logs, cache evidence, image identity, and environment notes.
  • State variance, timer, host/VM, and portability limitations honestly.

1. Scenario and acceptance contract

You inherit a small Docker build whose incremental feedback is unexpectedly slow. The runtime payload itself is tiny. Your checkpoint is to prove whether Dockerfile cache invalidation is the dominant bottleneck, improve only that dimension, and show that the conclusion survives repeated incremental builds.

Safety boundary. The checkpoint uses an isolated Buildx builder and exact ch40cp-* tags/files. Do not substitute a shared production builder or delete global caches. If your current context is not an authorized local/disposable daemon, stop.

2. Preflight and assumptions packet

set -eu
printf 'timestamp=%s
' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf 'context=%s
' "$(docker context show)"
docker version
docker info --format 'Server={{.ServerVersion}} Driver={{.Driver}} Cgroup={{.CgroupVersion}} CPUs={{.NCPU}} Memory={{.MemTotal}}'
docker compose version 2>/dev/null || true
docker buildx version

docker pull alpine:3.22
BASE_REF="$(docker image inspect alpine:3.22 --format '{{index .RepoDigests 0}}')"
printf 'base=%s
' "$BASE_REF" | tee ch40cp-base.txt

Also record whether this is native Linux or Docker Desktop, whether the Docker data/build cache lives on local or remote storage, and whether the target platform is native or emulated. Those facts bound the validity of the result.

3. Predict before changing anything

Prediction 1: after app-only edits, the baseline Dockerfile will re-execute the expensive dependency-simulation vertex because COPY . . precedes it.
Prediction 2: after reorganizing COPY boundaries, the optimized Dockerfile will keep that dependency vertex cached while still rebuilding the app-dependent vertex.
Invariant: base digest, synthetic workload, builder, target platform, runtime files, and benchmark procedure remain the same.

4. Create the exact source fixture

rm -rf ch40-checkpoint
mkdir ch40-checkpoint
cd ch40-checkpoint
printf 'dependency-set-v1
' > dependency.lock
printf 'app-v1
' > app.txt
printf '*.log
results/
' > .dockerignore
mkdir results

cat > Dockerfile.baseline <<'EOF'
# syntax=docker/dockerfile:1
ARG BASE
FROM ${BASE} AS build
WORKDIR /src
COPY . .
RUN i=0; while [ "$i" -lt 4500 ]; do       printf '%s:%s
' "$(cat dependency.lock)" "$i" | sha256sum >/dev/null;       i=$((i+1));     done  && mkdir -p /out && cp dependency.lock app.txt /out/
FROM ${BASE}
WORKDIR /app
COPY --from=build /out/ ./
CMD ["sh","-c","cat dependency.lock app.txt"]
EOF

cat > Dockerfile.optimized <<'EOF'
# syntax=docker/dockerfile:1
ARG BASE
FROM ${BASE} AS build
WORKDIR /src
COPY dependency.lock .
RUN i=0; while [ "$i" -lt 4500 ]; do       printf '%s:%s
' "$(cat dependency.lock)" "$i" | sha256sum >/dev/null;       i=$((i+1));     done
COPY app.txt .
RUN mkdir -p /out && cp dependency.lock app.txt /out/
FROM ${BASE}
WORKDIR /app
COPY --from=build /out/ ./
CMD ["sh","-c","cat dependency.lock app.txt"]
EOF

5. Create and bootstrap the dedicated builder

BUILDER=ch40cp-builder
if docker buildx inspect "$BUILDER" >/dev/null 2>&1; then
  echo "Checkpoint builder already exists; clean the prior checkpoint first." >&2
  exit 1
fi
docker buildx create --name "$BUILDER" --driver docker-container --use
docker buildx inspect "$BUILDER" --bootstrap | tee results/builder.txt
docker buildx du --builder "$BUILDER" | tee results/cache-before.txt

6. Warm both build graphs

BASE_REF="$(cat ../ch40cp-base.txt 2>/dev/null | sed 's/^base=//' || true)"
if [ -z "$BASE_REF" ]; then BASE_REF="$(docker image inspect alpine:3.22 --format '{{index .RepoDigests 0}}')"; fi

docker buildx build \
  --builder ch40cp-builder \
  --progress=plain \
  --load \
  --build-arg BASE="$BASE_REF" -f Dockerfile.baseline -t ch40cp:baseline .   2>&1 | tee results/baseline-warmup.log

docker buildx build \
  --builder ch40cp-builder \
  --progress=plain \
  --load \
  --build-arg BASE="$BASE_REF" -f Dockerfile.optimized -t ch40cp:optimized .   2>&1 | tee results/optimized-warmup.log

The warm-up is not part of the incremental comparison. Its job is to establish both caches under the same builder.

7. Baseline incremental benchmark: three app-only edits

for n in 2 3 4; do
  printf 'app-v%s
' "$n" > app.txt
  START=$SECONDS
  docker buildx build --builder ch40cp-builder --progress=plain --load     --build-arg BASE="$BASE_REF" -f Dockerfile.baseline -t ch40cp:baseline .     >"results/baseline-$n.log" 2>&1
  ELAPSED=$((SECONDS-START))
  printf 'baseline run=%s seconds=%s
' "$n" "$ELAPSED" | tee -a results/times.txt
  grep -E 'COPY|RUN i=0|CACHED|DONE' "results/baseline-$n.log" | tail -20
 done

Preserve every run, not only the fastest. Confirm whether the expensive RUN actually executed after each app-only change. Timing without vertex evidence is insufficient.

8. Apply exactly one Docker-specific change

The change is Dockerfile input ordering: dependency metadata is copied before volatile application source. No base image, loop count, builder, platform, context ignore rules, or runtime payload changes. This is the controlled optimization.

9. Optimized incremental benchmark: same three app-only edits

for n in 5 6 7; do
  printf 'app-v%s
' "$n" > app.txt
  START=$SECONDS
  docker buildx build --builder ch40cp-builder --progress=plain --load     --build-arg BASE="$BASE_REF" -f Dockerfile.optimized -t ch40cp:optimized .     >"results/optimized-$n.log" 2>&1
  ELAPSED=$((SECONDS-START))
  printf 'optimized run=%s seconds=%s
' "$n" "$ELAPSED" | tee -a results/times.txt
  grep -E 'COPY|RUN i=0|CACHED|DONE' "results/optimized-$n.log" | tail -20
 done

docker buildx du --builder ch40cp-builder | tee results/cache-after.txt

The success criterion is primarily causal: the expensive dependency vertex remains cached after app-only changes. Wall time should usually improve, but a noisy host can obscure a small time delta; the cache evidence remains decisive for the Dockerfile behavior.

10. Verify final images and runtime behavior

docker image inspect ch40cp:baseline ch40cp:optimized   --format 'tags={{join .RepoTags ","}} id={{.Id}} size={{.Size}}' | tee results/images.txt

docker run --rm --name ch40cp-baseline-run ch40cp:baseline | tee results/baseline-runtime.txt
docker run --rm --name ch40cp-optimized-run ch40cp:optimized | tee results/optimized-runtime.txt

The tags may point to images built from different app versions because the incremental sequences differ; that is acceptable for this build-cache checkpoint. The required invariant is that both constructions preserve the same dependency/application file contract and exit successfully. In a production experiment, use the same source revision for final artifact comparison.

11. Optional runtime snapshot—do not confuse it with build evidence

docker run -d --name ch40cp-runtime --memory=64m --cpus=0.5 ch40cp:optimized sh -c 'sleep 30'
docker inspect ch40cp-runtime --format 'Image={{.Image}} NanoCpus={{.HostConfig.NanoCpus}} Memory={{.HostConfig.Memory}}'
docker stats --no-stream ch40cp-runtime | tee results/runtime-stats.txt
docker rm -f ch40cp-runtime

This proves the runtime envelope and stats tooling work, but it is not the optimization target. Keep build and runtime conclusions separate.

12. Evidence packet and report

Performance question: Why are app-only incremental builds slow?
Population: <native Linux / Desktop / remote; host/VM notes>
Base digest: <from ch40cp-base.txt>
Builder: <results/builder.txt>
Baseline runs: <results/times.txt baseline rows>
Optimized runs: <results/times.txt optimized rows>
Causal evidence: <baseline expensive RUN executed; optimized expensive RUN CACHED>
Cache usage: <cache-before.txt / cache-after.txt>
Image identity/size: <images.txt>
Runtime sanity: <runtime output>
One change: COPY dependency.lock before app.txt
Security/reliability changes: none intended
Timer limitation: Bash SECONDS, one-second resolution
Variance limitation: small synthetic workload; host background load uncontrolled
External-validity limit: repeat on production-like builder/storage before capacity decisions
Conclusion: <supported / not supported by evidence>

13. Verification checklist

Checkpoint passes only if:
  • Docker/context/builder/base identity was captured before measurement;
  • both graphs were warmed before incremental runs;
  • three baseline and three optimized app-only changes were measured;
  • plain progress proves the expensive dependency vertex’s cache behavior;
  • only COPY/input ordering changed;
  • the report states timer/variance/platform limitations;
  • no global cache, unrelated image, or unrelated container was deleted.

14. Bounded cleanup

docker rm -f ch40cp-runtime 2>/dev/null || true
docker image rm ch40cp:baseline ch40cp:optimized 2>/dev/null || true
docker buildx rm ch40cp-builder
cd ..
rm -rf ch40-checkpoint
rm -f ch40cp-base.txt

The benchmark logs under ch40-checkpoint/results are deleted by this cleanup. Copy the evidence packet elsewhere first if you intend to retain it. No broad Docker cleanup is required.

15. What Chapter 40 adds to the production operating model

You can now turn “Docker feels slow” into a measured, layer-specific hypothesis; separate cold, warm, and incremental builds; distinguish compressed image transfer from runtime footprint; interpret Docker stats alongside cgroup/host evidence; profile storage/network paths without overgeneralizing; and accept an optimization only after the same benchmark improves under a controlled change. Chapter 41 turns this evidence discipline into full Docker troubleshooting across daemon failures, networking, DNS, storage, permissions, builds, and container incidents.

Knowledge check

Why is the expensive RUN cache status more important than a one-second timing difference in this checkpoint?

What is the checkpoint’s one controlled change?

Why does the checkpoint warm both build graphs before measuring incremental edits?

If optimized times are not faster but the expensive step is CACHED, what should you conclude?

Which evidence prevents you from mistaking a runtime resource issue for a build-cache issue?

Next lesson

Next: Troubleshooting Docker: Daemon Failures, Networking, DNS, Storage, Permissions, Builds, and Container Incidents: Concepts, Architecture, and Mental Model

Continue with the next lesson in the course sequence and carry forward the evidence-first Docker operating model.

Official references and version notes

Baseline checked:

2026-09-22. Current upstream baseline used for version-sensitive explanations: Docker Engine/CLI 29.8.1, Compose 5.5.1, Buildx 0.37.1, and BuildKit 0.33.0. Executable labs record the learner’s actual Docker, Buildx, BuildKit, kernel/cgroup, image digest, and host/VM environment before interpreting any timing.

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
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.