Chapter 10Lesson 03~110 minutes

Multi-Stage Builds, Minimal Runtime Images, Distroless Patterns, Debug Stages, and Build Separation: Configuration, Design Choices, and Tradeoffs

Minimal-runtime design is a dependency and operability decision, not a contest for the fewest megabytes. This lesson compares scratch, Distroless, minimal distributions, and fuller distributions; static versus dynamic binaries; separate debug targets versus shipped shells; and stage/external-source copy boundaries with explicit security and recovery tradeoffs.

scratch vs distrolessStatic vs dynamicDebug strategyLeast privilegeTradeoffs

Learning objectives

  • Choose scratch, Distroless, a minimal distro, or a fuller distro according to concrete runtime dependencies and support requirements.
  • Choose static versus dynamic linkage based on runtime libraries, portability, patching, and observability rather than image size alone.
  • Design a debug strategy that keeps production images lean without removing the ability to diagnose incidents.
  • Choose named-stage, external-image, or named-context sources for COPY --from and bind each external source to a reviewable identity.
  • Connect minimal-runtime choices to least privilege, vulnerability management, provenance, rollback, and operational recovery.
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. Start with runtime requirements, not a favorite base

The correct final base follows from an inventory: Does the process require a dynamic loader? Which native libraries? CA roots? Timezone or locale data? Name-service configuration? A writable temporary directory? A users database? A shell as part of the supported operational contract? FIPS-validated crypto? Vendor support? Once those requirements are explicit, image choice becomes an engineering decision rather than folklore.

2. Base-image decision table

Runtime base Strength Typical limitation Use when
scratch Empty; maximal control of copied files No shell, package manager, CA roots, libc, tzdata, passwd/group unless copied Runtime dependencies are fully understood and deliberately supplied
Distroless static/base/cc Purpose-built runtime dependencies without normal distro tooling No ordinary shell/package manager in production variants You need known runtime pieces but want a narrow production surface
Minimal distro Familiar libc/package ecosystem and easier operational tooling More packages/files to patch and reason about Dynamic/native dependencies or on-image support tooling are part of the contract
Fuller distro Broad compatibility and vendor tooling Largest dependency/maintenance surface Compatibility/support requirements justify it and are explicitly owned

Current Distroless docs explicitly suffix Debian 13 image names. Referencing the distribution family makes upgrades intentional instead of relying on a future moving default.

3. Static versus dynamic is a dependency-management choice

A static executable can be excellent for scratch because the binary carries the code it needs from its linked libraries. But “static” does not mean “no dependencies”: the program may still need certificates, timezone data, configuration, DNS behavior provided by the host/kernel, writable directories, or external services. Some ecosystems also intentionally rely on dynamic native libraries for compatibility, security updates, plugins, or vendor support.

A dynamically linked executable copied into scratch can fail even though the binary file exists. The kernel may report an execution error because the ELF interpreter/dynamic loader named by the binary is missing. This is a runtime-filesystem dependency failure, not a Docker daemon failure.

4. Distroless: narrow runtime, explicit debugging

Current Distroless Debian 13 static includes CA certificates, tzdata, a root passwd entry, and /tmp; base-nossl adds glibc; base adds glibc plus SSL runtime dependencies; cc adds libgcc-related runtime pieces. Choose the family matching the program rather than assuming all Distroless images are equivalent.

Production variants intentionally lack a shell. The project publishes debug and debug-nonroot variants that add a BusyBox shell for investigation. This maps directly to the chapter principle: diagnosability belongs in a deliberate debug identity, not in an undocumented hot patch of the release image.

Entrypoint consequence. With no shell, use exec/vector form such as ENTRYPOINT ["/app"]. Shell-form commands implicitly require a shell interpreter.

5. Choose a debug strategy before the incident

Strategy Production image Diagnostic capability Tradeoff
Dedicated Dockerfile debug target Lean Shell/tools in separate target built from same artifact Must secure and version debug identity too
Distroless debug/debug-nonroot Lean Distroless Project-provided BusyBox path Debug tag is a distinct image; do not deploy accidentally
External ephemeral diagnostic container Unchanged Tools can inspect shared network/volume/namespace where authorized Requires careful namespace/privilege boundaries
Ship shell/tools in production Larger Immediate in-container access More software and a broader support/security surface

The course favors the first two patterns for ordinary containerized applications. Host-namespace joining or privileged diagnostic containers are security-sensitive and belong in separately authorized labs, not this chapter's mandatory path.

6. COPY --from source identity is part of provenance

--from=build refers to a named stage in the same build definition. It is easy to audit because the producing stage is visible nearby. --from=nginx:tag or another external image is also valid Dockerfile syntax, but that external reference becomes a supply-chain input. Pin it by digest when the copied bytes must be reproducible.

Named contexts from Chapter 08 provide another option: the source can be supplied separately with --build-context. This can keep large or independently versioned inputs outside the main context, but their identity and trust still need evidence.

7. Least privilege crosses filesystem ownership and process identity

Setting a non-root USER is necessary but not sufficient. The copied executable, configuration, certificates, writable directories, and mounted volumes must have permissions compatible with that identity. A minimal image makes accidental assumptions visible because there may be no startup script running as root to repair permissions behind the scenes.

FROM scratch AS runtime
COPY --from=build --chown=65532:65532 /out/app /app
USER 65532:65532
ENTRYPOINT ["/app"]

If the application needs a writable directory, create/copy it deliberately or mount one with controlled ownership. Do not switch the process to root merely because a path was packaged incorrectly.

8. Smaller surface helps, but security evidence remains multidimensional

Removing compilers and package managers can reduce exploitable utilities, scanner noise, transfer size, and patch burden. Yet the application binary and its libraries still have vulnerabilities; base images and copied artifacts still need provenance; runtime Linux capabilities and seccomp/LSM controls still matter; and a signed image still needs vulnerability and runtime-health evaluation.

Therefore record at least: final digest, source/build identity, exact base digest, artifact checksum, runtime user, copied dependency list, scan result when a scanner is part of your environment, and the exception rationale for any additional tooling.

9. Worked decision: outbound-TLS Go service

Suppose a Go service is compiled with CGO_ENABLED=0, has no plugins, writes only to stdout, but calls public HTTPS APIs and needs timezone conversions. Plain scratch is insufficient unless you deliberately copy CA roots and timezone data. Current Distroless static-debian13 already includes CA certificates and tzdata, so it can be a strong candidate. A minimal distro is also defensible if your organization requires package-managed runtime inspection or a supported shell-based runbook.

The decision is justified by runtime dependencies and operating policy, not by one universal “best” base.

10. Decision exercise

Choose a base for each: (a) a static CLI-style service with no TLS/timezone needs; (b) a dynamically linked C++ service requiring libstdc++, CA roots, and no shell; (c) a vendor agent whose support procedure requires package-manager diagnostics. State the prerequisite evidence and a debugging strategy for each.

Next lesson

Next: Diagnostics, Failure Modes, Security, and Performance

Localize failures created by missing interpreters, loaders, libraries, certificates, ownership, and accidental build-environment leakage.

Knowledge check

When is scratch a poor choice even for a tiny binary?

Why might Distroless static fit a static HTTPS client better than empty scratch?

Is a debug target a “development backdoor” into the production image?

What new trust issue appears with COPY --from=external-image:tag?

Why can switching USER back to root hide rather than solve a failure?

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.