Multi-Stage Builds

Multi-stage builds are a Docker feature that allows you to use multiple FROM statements in a single Dockerfile. This separates build-time dependencies from runtime dependencies, resulting in dramatically smaller, more secure production images—often reducing image size by 80-95%.

Build Stage Runtime Stage 90% Size Reduction
What Are Multi-Stage Builds?

Multi-stage builds allow you to use multiple FROM statements in a single Dockerfile. Each FROM begins a new stage. You can copy artifacts from earlier stages into later stages, leaving behind everything that isn't needed in the final image. This solves the classic Dockerfile dilemma: use a large image with build tools (compilers, SDKs, npm, etc.) to build your application, then copy only the compiled artifacts into a minimal runtime image.

Before multi-stage builds, developers often used complex shell scripts to build, then copy artifacts, or maintained separate Dockerfiles for development and production. Multi-stage builds solve this elegantly—everything stays in one file, and the final image contains only what's necessary to run the application.

Stage 1: Builder ┌─────────────────────────────┐ │ FROM golang:1.21 (1.2GB) │ │ - Go compiler │ │ - Full build tools │ │ - Source code │ │ - Compile myapp │ └─────────────┬───────────────┘ │ COPY --from=builder ↓ Stage 2: Runtime (Final) ┌─────────────────────────────┐ │ FROM alpine:latest (5MB) │ │ - Only compiled binary │ │ - No compilers │ │ - No source code │ │ - Final image: ~15MB │ └─────────────────────────────┘
A typical Go application built with multi-stage builds goes from 1.2GB (full build image) to 15MB (production image)—a 98.7% reduction. For Node.js, you can go from 800MB to under 200MB.
The Problem: Fat Production Images

Before multi-stage builds, Dockerfiles often looked like this: using a full SDK base image, installing build tools, compiling the application, and then—all those build tools and intermediate files stayed in the final image. The image contained compilers, package managers, source code, and debug symbols—none of which are needed to run the application. This resulted in huge images (1GB+), slower deployments, larger attack surfaces, and higher storage costs.

# Without multi-stage builds (BAD) FROM node:18 WORKDIR /app COPY package*.json ./ RUN npm install # Installs devDependencies too COPY . . RUN npm run build # Build tools stay in image EXPOSE 3000 CMD ["npm", "start"] # Result: 900MB+ image containing compilers, source code, dev dependencies
Basic Syntax: How Multi-Stage Builds Work

The syntax is simple: use multiple FROM statements. Each stage can have a name (using AS name). To copy files from a previous stage, use COPY --from=stage_name. Only the last stage's contents are included in the final image—all previous stages are discarded.

# Multi-stage build template # Stage 1: Build (named "builder") FROM node:18-alpine AS builder WORKDIR /build COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npm run build # Stage 2: Production (final stage) FROM node:18-alpine WORKDIR /app # Copy only what we need from builder COPY --from=builder /build/dist ./dist COPY --from=builder /build/node_modules ./node_modules CMD ["node", "dist/server.js"] # The builder stage is discarded; only final stage is in the image
Example 1: Go Application (90% Size Reduction)
# Multi-stage build for Go # Stage 1: Build FROM golang:1.21-alpine AS builder WORKDIR /app # Copy go mod files first (caching) COPY go.mod go.sum ./ RUN go mod download # Copy source and build COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . # Stage 2: Runtime (minimal) FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ # Copy binary from builder COPY --from=builder /app/main . EXPOSE 8080 CMD ["./main"] # Without multi-stage: ~1.2GB # With multi-stage: ~15MB # Reduction: 98.7%
Go applications benefit tremendously from multi-stage builds because Go binaries are statically linked. The final image needs only the binary and Alpine base.
Example 2: Node.js Application (Build + Production Stages)
# Multi-stage build for Node.js with build tools # Stage 1: Build (includes dev dependencies for building) FROM node:18-alpine AS builder WORKDIR /build # Copy package files COPY package*.json ./ COPY yarn.lock ./ # Install ALL dependencies (including dev) RUN npm ci # Copy source and build COPY . . RUN npm run build RUN npm prune --production # Remove dev dependencies # Stage 2: Production (only what's needed to run) FROM node:18-alpine WORKDIR /app # Copy production artifacts only COPY --from=builder /build/package*.json ./ COPY --from=builder /build/node_modules ./node_modules COPY --from=builder /build/dist ./dist ENV NODE_ENV=production EXPOSE 3000 # Create non-root user RUN addgroup -g 1001 -S nodejs && \ adduser -S nodejs -u 1001 USER nodejs CMD ["node", "dist/server.js"] # Without multi-stage: ~900MB # With multi-stage: ~150MB # Reduction: 83%
Example 3: Python Application (Wheels and Virtual Environments)
# Multi-stage build for Python # Stage 1: Build wheels FROM python:3.11-slim AS builder WORKDIR /app # Install build dependencies RUN apt-get update && \ apt-get install -y --no-install-recommends gcc python3-dev && \ rm -rf /var/lib/apt/lists/* # Copy requirements and build wheels COPY requirements.txt . RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt # Stage 2: Runtime FROM python:3.11-slim WORKDIR /app # Copy wheels from builder and install COPY --from=builder /app/wheels /wheels COPY --from=builder /app/requirements.txt . RUN pip install --no-cache /wheels/* # Copy application code COPY . . ENV PYTHONUNBUFFERED=1 EXPOSE 8000 CMD ["python", "app.py"] # Without multi-stage: ~900MB # With multi-stage: ~120MB # Reduction: 86%
Example 4: Java Application (Maven/Gradle)
# Multi-stage build for Java with Maven # Stage 1: Build with Maven FROM maven:3.9-eclipse-temurin-17-alpine AS builder WORKDIR /app # Copy pom.xml first for caching COPY pom.xml . RUN mvn dependency:go-offline # Copy source and build COPY src ./src RUN mvn package -DskipTests # Stage 2: Runtime (JRE only, no build tools) FROM eclipse-temurin:17-jre-alpine WORKDIR /app # Copy JAR from builder COPY --from=builder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] # Without multi-stage: ~700MB (includes Maven, JDK) # With multi-stage: ~120MB (only JRE + JAR) # Reduction: 83%
Advanced Multi-Stage Techniques

Multiple Build Stages for Different Environments: You can have separate stages for development, testing, and production. This keeps your Dockerfile clean while supporting different use cases.

Parallel Build Stages: Docker builds stages in parallel when possible. If you have independent build stages, they'll run simultaneously, reducing total build time.

Stop at a Specific Stage: Use --target to build only up to a specific stage: docker build --target builder -t myapp:builder .

# Multiple stages for different environments FROM node:18-alpine AS base WORKDIR /app FROM base AS deps COPY package*.json ./ RUN npm ci FROM deps AS dev COPY . . CMD ["npm", "run", "dev"] FROM deps AS builder COPY . . RUN npm run build FROM nginx:alpine AS prod COPY --from=builder /app/dist /usr/share/nginx/html # Build different stages: # docker build --target dev -t myapp:dev . # docker build --target prod -t myapp:prod .
Before vs After: Real Size Comparisons
Language/FrameworkWithout Multi-StageWith Multi-StageReduction
Go (static binary)1.2 GB (full SDK + build tools)15 MB (Alpine + binary)98.7%
Node.js (TypeScript)900 MB (includes devDependencies, source)150 MB (only built files, production deps)83%
Python (with C extensions)950 MB (includes build tools, compilers)180 MB (only runtime + compiled libraries)81%
Java (Spring Boot)700 MB (includes Maven, JDK)180 MB (JRE + JAR) 74%
Rust/React combined2 GB (Node + Rust compilers)200 MB90%
Named Stages and Selective Copying

Stages can be named with AS. This makes your Dockerfile more readable and allows you to reference specific stages by name. You can copy from any previous stage, not just the immediate previous one. You can also copy multiple times from different stages.

# Advanced copying between stages FROM node:18-alpine AS frontend-builder WORKDIR /frontend COPY frontend/package*.json ./ RUN npm ci COPY frontend/ ./ RUN npm run build FROM maven:3.9-eclipse-temurin AS backend-builder WORKDIR /backend COPY backend/pom.xml ./ RUN mvn dependency:go-offline COPY backend/ ./ RUN mvn package FROM nginx:alpine # Copy frontend static files COPY --from=frontend-builder /frontend/dist /usr/share/nginx/html # Copy backend JAR (if needed for API) COPY --from=backend-builder /backend/target/*.jar /app/app.jar EXPOSE 80
Security Benefits of Multi-Stage Builds

Multi-stage builds improve security in several ways. First, they eliminate build tools, compilers, and package managers from production images. These tools are common attack vectors and aren't needed at runtime. Second, they reduce the attack surface—smaller images have fewer binaries and libraries that could contain vulnerabilities. Third, they exclude source code from the final image, protecting proprietary code from potential container escapes.

A production image should contain ONLY your compiled application and its runtime dependencies. No compilers, no package managers, no build scripts, no source code. Multi-stage builds make this easy to achieve.
Multi-Stage Build Best Practices
  • Name your stages using AS stage_name for clarity and easier reference.
  • Copy only what you need from previous stages. Be specific with paths—avoid copying entire directories if you only need a subdirectory.
  • Use minimal base images for the final stage (Alpine, Distroless, or slim variants).
  • Order stages for caching - Put less frequently changed stages first, more frequently changed later.
  • Use multi-stage even for simple apps - The benefits almost always outweigh the slight complexity increase.
  • Test each stage independently using --target to verify builds at each step.
  • Clean up package caches in the builder stage before copying to final stage.
Frequently Asked Questions
Do intermediate stages increase build time?
Not significantly. The builder stage runs once, and its layers are cached. Subsequent builds are fast because unchanged layers are reused. The final stage just copies artifacts, which is very fast.
Can I use different base images for different stages?
Yes! That's the power of multi-stage builds. Your builder stage might use a full SDK image (e.g., node:18), while your final stage uses Alpine (node:18-alpine). You can even use completely different OSes (build on Ubuntu, run on Distroless).
How many stages can I have in a Dockerfile?
There's no hard limit. You can have as many as you need. Common patterns use 2-4 stages: base, dependencies, builder, production. More stages can help organize complex builds.
Can I copy from external images, not just previous stages?
Yes! You can copy from any image, even ones not built in your Dockerfile. Example: COPY --from=nginx:alpine /etc/nginx/nginx.conf /nginx.conf. This is useful for pulling configuration or binaries from official images.
Do I need to clean up intermediate images?
Docker automatically discards intermediate stages—they don't appear in docker images and won't persist on disk. Only the final stage's layers are kept. However, the builder stage's layers are cached to speed up subsequent builds.
Can I stop a build at a specific stage?
Yes: docker build --target stage_name -t myapp:stage . This is useful for debugging or creating development images. For example, you can build the builder stage separately to inspect intermediate files.
Why is my multi-stage build still large?
You might be copying more than you need. Check your COPY --from instructions—are you copying entire directories? Are you copying node_modules when you only need dist/ and package.json? Also ensure you're using a minimal base image for the final stage.
Does multi-stage work with Docker BuildKit?
Yes, and BuildKit enhances multi-stage builds with additional features like parallel stage building, better caching, and mount secrets. Enable BuildKit with DOCKER_BUILDKIT=1 docker build ..
Previous: Dockerfile Best Practices Next: Image Tagging & Registries

Multi-stage builds are essential for production-grade Docker images. They produce smaller, more secure images that deploy faster and reduce infrastructure costs.