Multi-Stage Builds
๐๏ธ Multi-Stage Builds
docker1's Dockerfile chapter (docker_06) built a single-stage image. Three courses have since referenced multi-stage builds without ever teaching the pattern โ express2-8's deployment chapter, node3-8's production chapter, and pipelines1-6's Docker-in-CI chapter. This is the deep dive all three assumed.
What docker1's Single-Stage Dockerfile Leaves on the Table
A single-stage build bundles everything used during the build โ dev dependencies, build tools, compilers, source files โ directly into the same image that ends up running in production, even though most of that is only needed to produce the runnable output, not to actually run it. The result: a bloated image, and a larger attack surface sitting in production for no functional reason.
The Multi-Stage Pattern
A Dockerfile can contain multiple FROM statements, each starting a new, independent stage. A later stage can selectively copy just the specific build artifacts it needs from an earlier one via COPY --from=<stage> โ discarding everything else (build tools, source code, dev dependencies) from the final image entirely.
Single-Stage vs. Multi-Stage Dockerfile
Single-Stage (docker1's approach)
Multi-Stage
The final image is built FROM node:18-slim โ a fresh, minimal starting point โ and only ever receives the specific files the COPY --from=builder lines explicitly select. Everything else that existed in the builder stage (the full source tree, the TypeScript compiler, dev-only dependencies) never makes it into the image that actually ships.
Naming Stages & Selective Copying
AS builder names a stage so later stages can reference it by name rather than a fragile numeric index. docker build --target builder can also build only up to a named stage โ useful for running tests against the build stage without needing the final lean image at all.
| Concept | Purpose |
|---|---|
FROM ... AS name | Starts a named stage |
COPY --from=name | Copies specific files from a named stage into the current one |
--target name | Builds only up to a specific named stage |
A real-world illustration of the payoff: a Node.js/TypeScript app built single-stage might land around 1.2GB once dev dependencies, the compiler, and source files are included โ the same app built multi-stage, copying only the compiled output and production dependencies into a slim final image, can easily land under 150MB.
Why This Pattern Recurs Across the Site
express2-8's deployment chapter and node3-8's production chapter both already used multi-stage Dockerfiles as part of their own deployment examples, and pipelines1-6's CI pipeline built and pushed multi-stage images โ all three assumed the reader already understood the pattern shown above. This chapter is the missing explanation those three examples were quietly built on top of.
๐ป Coding Challenges
Challenge 1: Convert a Single-Stage Dockerfile
Convert this single-stage Dockerfile into a multi-stage one: FROM python:3.12, installs build dependencies, compiles a Cython extension, runs the app. The final image should only contain the compiled extension and runtime dependencies, not the build toolchain.
Goal: Practice splitting a build-and-run Dockerfile into a builder stage and a lean final stage.
Challenge 2: Explain the Size Reduction
Explain, in your own words, why copying only /app/dist and production node_modules from a builder stage produces a smaller image than copying the entire builder stage's filesystem.
Goal: Practice articulating exactly what a multi-stage build excludes and why that exclusion is the whole point.
Challenge 3: Use --target for Testing
Given this chapter's two-stage Dockerfile, write the docker build command that builds only the builder stage, useful for running tests against it without building the final slim image.
Goal: Practice using --target to build a specific intermediate stage.
It's easy to write COPY --from=builder /app ./ โ copying the entire builder stage's working directory โ instead of the specific files actually needed. Doing this brings the source tree, dev dependencies, and build artifacts right back into the final image, quietly undoing the size and attack-surface reduction multi-stage builds exist to provide. Always name build stages explicitly with AS (an unnamed stage can only be referenced by a fragile numeric index that breaks if stages are reordered), and be deliberate about exactly which files each COPY --from line selects โ the discipline of copying only what's needed is the entire value of this pattern, not an optional refinement.
๐ฏ What's Next
The next chapter is Image Optimization & Layer Caching โ minimizing layers, .dockerignore, choosing a base image (alpine vs. slim vs. full), and build cache invalidation order.