Devops

Multi-Stage Builds: Beyond Basics

Multi-stage builds are Docker's answer to the "build vs runtime" dilemma. You've seen the basics - separate build and runtime stages. This article goes deeper: parallel stages, conditional builds, testing stages, and patterns that make complex builds manageable.

πŸ“‹ At a Glance

AspectDetails
TopicAdvanced multi-stage patterns, testing stages, secrets handling
ComplexityIntermediate-Advanced
PrerequisitesPart 3 (Build Process), Part 5 (Optimization)
Key InsightStages are independent build graphs - use them for more than size
Time to Master2-3 hours

🎯 What You'll Learn

  • Advanced stage patterns - parallel builds, conditional stages, shared bases
  • Testing in builds - test stages, fail-fast builds, coverage extraction
  • Secrets management - build-time secrets that never reach images
  • Dynamic staging - building different outputs from same Dockerfile
  • Real-world patterns - monorepo builds, polyglot applications

πŸ”₯ Production Story: The Leaked Credentials

A security audit found NPM tokens in a production image. The team was confused - they had deleted the .npmrc file after npm install.
The Dockerfile:
DOCKERFILE(7 lines)
Code
Loading syntax highlighter...
The audit finding:
BASH(6 lines)
Code
Loading syntax highlighter...
The .npmrc was deleted in layer 4, but it still existed in layer 2. Docker layers are additive - deletion only hides, never removes.
The fix using multi-stage:
DOCKERFILE(10 lines)
Code
Loading syntax highlighter...
Lesson: Multi-stage builds aren't just for size reduction - they're a security boundary. Secrets in build stages never reach the final image.

🧠 Mental Model: Stages as Build Graphs

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     MULTI-STAGE BUILD GRAPH                             β”‚
β”‚                                                                         β”‚
β”‚  Traditional view: Linear pipeline                                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”                                  β”‚
β”‚  β”‚buildβ”‚ β†’ β”‚test β”‚ β†’ β”‚lint β”‚ β†’ β”‚finalβ”‚                                  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”˜                                  β”‚
β”‚                                                                         β”‚
β”‚  Reality: Directed Acyclic Graph (DAG)                                  β”‚
β”‚                                                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                                            β”‚
β”‚  β”‚  base   β”‚ ← Shared foundation                                        β”‚
β”‚  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜                                                            β”‚
β”‚       β”‚                                                                 β”‚
β”‚       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                   β”‚
β”‚       β–Ό              β–Ό              β–Ό                                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                β”‚
β”‚  β”‚  deps   β”‚   β”‚  test   β”‚   β”‚  lint   β”‚  ← Run in parallel!            β”‚
β”‚  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜                                β”‚
β”‚       β”‚             β”‚             β”‚                                     β”‚
β”‚       β”‚             β–Ό             β–Ό                                     β”‚
β”‚       β”‚        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                 β”‚
β”‚       β”‚        β”‚ report  β”‚  β”‚  check  β”‚  ← Optional outputs             β”‚
β”‚       β”‚        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                 β”‚
β”‚       β”‚                                                                 β”‚
β”‚       └──────────────┬──────────────────                                β”‚
β”‚                      β–Ό                                                  β”‚
β”‚                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                             β”‚
β”‚                 β”‚  final  β”‚  ← Production image                         β”‚
β”‚                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                             β”‚
β”‚                                                                         β”‚
β”‚  BuildKit builds independent branches in PARALLEL                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Key insight: Stages without dependencies on each other build simultaneously. Design your Dockerfile to maximize parallelism.

πŸ”¬ Deep Dive

Stage Fundamentals Revisited

DOCKERFILE(12 lines)
Code
Loading syntax highlighter...
Stage properties:
  • Each stage starts fresh (empty filesystem)
  • Stages can be named (AS name) or numbered (0, 1, 2...)
  • Only the final stage becomes the output image
  • COPY --from= extracts files from other stages
  • ARG values reset after each FROM

Parallel Stage Execution

BuildKit automatically parallelizes independent stages:

DOCKERFILE(25 lines)
Code
Loading syntax highlighter...
Execution timeline:
Time ────────────────────────────────────────────────────────►

frontend: [npm ci]──────[build]─────────────────┐
                                                β”‚
backend:  [pip install]─[pytest]────────────────┼───┐
                                                β”‚   β”‚
tools:    [go build]────────────────────────────┼───┼──┐
                                                β”‚   β”‚  β”‚
final:                                          └───┴──┴─[copy all]

Total time = max(frontend, backend, tools) + final copy
NOT: frontend + backend + tools + final (sequential)

Shared Base Stages

Avoid duplicating common setup:

DOCKERFILE(31 lines)
Code
Loading syntax highlighter...

Testing Stages

Fail-fast testing:
DOCKERFILE(26 lines)
Code
Loading syntax highlighter...
Extract test artifacts:
DOCKERFILE(11 lines)
Code
Loading syntax highlighter...
Conditional test execution:
DOCKERFILE(19 lines)
Code
Loading syntax highlighter...
BASH(5 lines)
Code
Loading syntax highlighter...

Secrets in Multi-Stage Builds

BuildKit secrets (recommended):
DOCKERFILE(16 lines)
Code
Loading syntax highlighter...
BASH(2 lines)
Code
Loading syntax highlighter...
SSH agent forwarding:
DOCKERFILE(15 lines)
Code
Loading syntax highlighter...
BASH(2 lines)
Code
Loading syntax highlighter...
Stage isolation for secrets:
DOCKERFILE(20 lines)
Code
Loading syntax highlighter...

Dynamic Stage Selection

Building different outputs:
DOCKERFILE(28 lines)
Code
Loading syntax highlighter...
BASH(4 lines)
Code
Loading syntax highlighter...

Monorepo Patterns

Shared dependencies:
monorepo/
β”œβ”€β”€ packages/
β”‚   β”œβ”€β”€ common/
β”‚   β”œβ”€β”€ api/
β”‚   └── web/
β”œβ”€β”€ package.json
└── Dockerfile
DOCKERFILE(38 lines)
Code
Loading syntax highlighter...
BASH(3 lines)
Code
Loading syntax highlighter...

COPY --from External Images

You can copy from any image, not just build stages:

DOCKERFILE(13 lines)
Code
Loading syntax highlighter...

Build Arguments Across Stages

DOCKERFILE(18 lines)
Code
Loading syntax highlighter...
BASH(5 lines)
Code
Loading syntax highlighter...

⚠️ Common Mistakes

Mistake 1: Not Using Named Stages

DOCKERFILE(13 lines)
Code
Loading syntax highlighter...

Mistake 2: Forgetting ARG Scope

DOCKERFILE(10 lines)
Code
Loading syntax highlighter...

Mistake 3: Copying More Than Needed

DOCKERFILE(10 lines)
Code
Loading syntax highlighter...

πŸ› Debug This: The Missing Environment Variable

A developer reports: "My app works in the build stage but fails in production. Environment variables are missing!"

DOCKERFILE(13 lines)
Code
Loading syntax highlighter...
Why is API_URL missing in the final stage?

βœ… Solution:

ENV variables don't transfer between stages. Each stage is independent.

The problem: ENV API_URL was set in builder stage, but the final stage starts fresh from node:20-alpine with no knowledge of the builder's environment.
Fix 1: Redeclare in final stage:
DOCKERFILE(10 lines)
Code
Loading syntax highlighter...
Fix 2: Use runtime configuration:
DOCKERFILE(9 lines)
Code
Loading syntax highlighter...
Fix 3: Configuration file:
DOCKERFILE(9 lines)
Code
Loading syntax highlighter...
Key lesson: Stages share nothing automatically. COPY --from transfers files, not environment.

πŸ’» Exercises

Exercise 1: Parallel Stage Build

⭐ Difficulty: Easy | ⏱️ Time: 15 minutes

DOCKERFILE(8 lines)
Code
Loading syntax highlighter...
BASH(16 lines)
Code
Loading syntax highlighter...

Exercise 2: Test Stage with Artifacts

⭐⭐ Difficulty: Medium | ⏱️ Time: 20 minutes

BASH(19 lines)
Code
Loading syntax highlighter...

Exercise 3: Secret Handling

⭐⭐ Difficulty: Medium | ⏱️ Time: 20 minutes

BASH(15 lines)
Code
Loading syntax highlighter...

Exercise 4: Monorepo Multi-Service Build

⭐⭐⭐ Difficulty: Hard | ⏱️ Time: 30 minutes

BASH(22 lines)
Code
Loading syntax highlighter...

Exercise 5: Complete CI/CD Pipeline Dockerfile

⭐⭐⭐⭐ Difficulty: Expert | ⏱️ Time: 35 minutes

Create a comprehensive Dockerfile with these stages:

deps-base β†’ deps-dev β†’ lint ──┐
                              β”‚
         β†’ deps-prod          β”œβ”€β†’ build β†’ production
                              β”‚
              test β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    ↓
              coverage (extractable)

Requirements:

  1. deps-base: Install system deps
  2. deps-dev: Full dev dependencies
  3. deps-prod: Production only
  4. lint: ESLint check (fails build if errors)
  5. test: Jest tests (fails build if tests fail)
  6. coverage: Extractable coverage report
  7. build: TypeScript compile
  8. production: Minimal runtime image
BASH(8 lines)
Code
Loading syntax highlighter...

🎀 Senior-Level Interview Questions

Q1: How do you handle secrets in multi-stage Docker builds?

Strong Answer:

"There are several patterns depending on the use case:

BuildKit secret mounts (best practice):
DOCKERFILE
Code
Loading syntax highlighter...
Secret is available during build but never written to any layer. Build with --secret id=npmrc,src=.npmrc.
Stage isolation: Even with ARG, use a separate build stage that's discarded:
DOCKERFILE(7 lines)
Code
Loading syntax highlighter...
SSH forwarding for git:
DOCKERFILE
Code
Loading syntax highlighter...
What NOT to do:
  • Don't use ARG for secrets in final stage (visible in history)
  • Don't COPY secret files (persist in layers)
  • Don't delete secrets after use in same stage (still in earlier layer)

The key principle: secrets should exist only during specific RUN commands, never in any layer that becomes part of the final image."

Q2: Explain how BuildKit parallelizes multi-stage builds.

Strong Answer:

"BuildKit represents the Dockerfile as a directed acyclic graph (DAG) and executes independent branches in parallel.

How it determines parallelism:
  1. Parses all stages and their dependencies (COPY --from, FROM...AS)
  2. Builds dependency graph
  3. Stages with no dependencies on each other run concurrently
Example:
DOCKERFILE(9 lines)
Code
Loading syntax highlighter...

Without parallelism: 30 + 30 + copy = 61s With parallelism: max(30, 30) + copy = 31s

Maximizing parallelism:
  • Keep stages independent when possible
  • Use shared base stages to avoid duplicate work
  • Don't create unnecessary dependencies between stages
Viewing parallelism:
BASH
Code
Loading syntax highlighter...

Shows which stages run simultaneously.

Practical impact: Complex builds with frontend, backend, and tools can see 2-3x speedup. In our CI, we reduced builds from 15 minutes to 5 minutes primarily through parallel stages."

Q3: When would you use multiple FROM stages vs just combining RUN commands?

Strong Answer:

"They solve different problems:

Multiple stages for:
  1. Size reduction - Build tools not needed at runtime
  2. Security isolation - Secrets in build stage only
  3. Different base images - Full SDK for build, minimal for runtime
  4. Parallel builds - Independent stages run concurrently
  5. Multiple outputs - Same Dockerfile, different --target builds
Combined RUN for:
  1. Layer optimization - Install and clean in one layer
  2. Cache efficiency - Logically grouped operations
  3. Simple linear operations - No benefit from separation
Decision framework:
Need different base image? β†’ Multi-stage
Handling secrets? β†’ Multi-stage
Want parallel builds? β†’ Multi-stage
Building multiple artifacts? β†’ Multi-stage
Just installing packages? β†’ Combined RUN
Simple cleanup? β†’ Combined RUN
Example where both matter:
DOCKERFILE(7 lines)
Code
Loading syntax highlighter...

I use multi-stage as the default for production images and combine RUNs within each stage for optimization."

Q4: How would you structure a Dockerfile for a monorepo with shared dependencies?

Strong Answer:

"Monorepo Dockerfiles need to balance build speed, parallelism, and caching:

Pattern 1: Shared base with workspace installs:
DOCKERFILE(14 lines)
Code
Loading syntax highlighter...
Key considerations:
  1. Layer caching - Copy package.jsons first, then source
  2. Shared code - Build shared libraries first, copy to dependents
  3. Parallelism - Independent services build in parallel
  4. Selective builds - Use --target to build specific services
  5. Cache mounts - Share npm/pip cache across stages
For very large monorepos:
  • Consider using Docker buildx bake for coordinated builds
  • Use build matrix in CI for service-specific builds
  • Implement change detection to skip unchanged services
Production deployment: Each service gets its own minimal image, built from the same Dockerfile with different --target flags. Shared base ensures consistency."

Q5: How do you debug issues in earlier build stages when only the final stage fails?

Strong Answer:

"Several techniques:

1. Build up to specific stage:
BASH(2 lines)
Code
Loading syntax highlighter...
2. Add debug output in Dockerfile:
DOCKERFILE(2 lines)
Code
Loading syntax highlighter...
3. Use BuildKit progress output:
BASH
Code
Loading syntax highlighter...

Shows all output including from cached layers.

4. Extract artifacts from intermediate stage:
DOCKERFILE(3 lines)
Code
Loading syntax highlighter...
BASH
Code
Loading syntax highlighter...
5. Add a debug stage:
DOCKERFILE(4 lines)
Code
Loading syntax highlighter...
6. Override entrypoint:
BASH
Code
Loading syntax highlighter...
7. BuildKit cache inspection:
BASH
Code
Loading syntax highlighter...

My usual flow: build with --progress=plain, identify failing step, build --target to that stage, shell in and investigate."


πŸ“ Summary & Key Takeaways

Core Concepts

ConceptKey Point
Stage independenceEach stage starts fresh, shares nothing automatically
Parallel executionIndependent stages build concurrently
Named stagesUse AS name for maintainability
COPY --fromOnly way to transfer files between stages
--targetBuild up to specific stage

Multi-Stage Patterns

DOCKERFILE(14 lines)
Code
Loading syntax highlighter...

What You Can Do Now

  1. Structure builds for parallelism - Independent stages for speed
  2. Handle secrets safely - BuildKit mounts or stage isolation
  3. Create multi-output Dockerfiles - One file, many targets
  4. Debug intermediate stages - --target and artifact extraction

πŸ“‹ Quick Reference

Stage Syntax

DOCKERFILE(16 lines)
Code
Loading syntax highlighter...

BuildKit Secret/SSH

DOCKERFILE(13 lines)
Code
Loading syntax highlighter...

Debug Commands

BASH(11 lines)
Code
Loading syntax highlighter...

πŸ“… Review Schedule

DayTaskTime
Day 1Review stage isolation principles10 min
Day 3Do Exercise 1 (parallel stages)15 min
Day 7Implement testing stage in a project25 min
Day 14Add BuildKit secrets to a build20 min
Day 30Refactor a complex Dockerfile with multiple targets30 min

πŸ“š Series Navigation

PreviousCurrentNext
Part 5: Dockerfile OptimizationPart 6: Multi-Stage BuildsPart 7: Base Image Selection
Docker Compendium Series: