All skills
Skillintermediate

Multi-Stage Docker Builds

Multi-stage builds allow you to use multiple FROM statements in your Dockerfile. Each FROM instruction can use a different base, and each begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don't want in the final image.

Claude Code Knowledge Pack7/10/2026

Overview

Multi-Stage Docker Builds

Overview

Multi-stage builds allow you to use multiple FROM statements in your Dockerfile. Each FROM instruction can use a different base, and each begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don't want in the final image.

Benefits:

  • Smaller images: 50-85% size reduction
  • Separation of concerns: Build vs runtime environments
  • Better security: No build tools in production images
  • Faster deployments: Smaller images transfer faster
  • Cleaner builds: No manual cleanup scripts needed

Basic Syntax

# syntax=docker/dockerfile:1

# Stage 1: Named "builder"
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp

# Stage 2: Final production image
FROM alpine:3.21
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["myapp"]

Stage Naming

Explicit Names (Recommended)

# Name stages explicitly for clarity
FROM node:20 AS dependencies
RUN npm install

FROM node:20 AS builder
COPY --from=dependencies /app/node_modules ./node_modules
RUN npm run build

FROM node:20-alpine AS production
COPY --from=builder /app/dist ./dist

Numeric References (Less Clear)

# Reference previous stages by number (0-indexed)
FROM node:20
RUN npm install

FROM node:20
COPY --from=0 /app/node_modules ./node_modules
RUN npm run build

FROM node:20-alpine
COPY --from=1 /app/dist ./dist

Common Patterns

Pattern 1: Build and Runtime Separation

Use Case: Compiled languages (Go, Rust, C++, Java)

# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags="-s -w" -o main .

# Runtime stage
FROM scratch
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]

Size Impact:

  • Builder stage: ~300MB
  • Final image: ~8MB
  • Reduction: 97%

Pattern 2: Dependency Installation

Use Case: Separate dependency installation from application code

# Dependency stage
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]

Benefits:

  • Cached dependency layer (only rebuilds when package.json changes)
  • Production dependencies only in final image
  • Faster builds with layer caching

Pattern 3: Test Stage

Use Case: Run tests without including test dependencies in final image

# Dependency stage
FROM python:3.12-slim AS deps
WORKDIR /app
COPY requirements.txt requirements-dev.txt ./
RUN pip install --user -r requirements.txt

# Test stage
FROM deps AS test
RUN pip install --user -r requirements-dev.txt
COPY . .
RUN pytest tests/

# Production stage
FROM python:3.12-slim AS production
WORKDIR /app
COPY --from=deps /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]

Build with tests:

docker build --target test -t myapp:test .

Build production (tests automatically run before this stage):

docker build -t myapp:latest .

Pattern 4: Multi-Architecture

Use Case: Build for different platforms

FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
ARG TARGETARCH
ARG TARGETOS
WORKDIR /app
COPY . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o app

FROM alpine:3.21
COPY --from=builder /app/app /app
ENTRYPOINT ["/app"]
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .

Pattern 5: Development vs Production

Use Case: Different images for dev and prod

# Base stage with common dependencies
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
RUN npm ci

# Development stage
FROM base AS development
ENV NODE_ENV=development
COPY . .
CMD ["npm", "run", "dev"]

# Build stage
FROM base AS builder
COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine AS production
ENV NODE_ENV=production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]

Build for development:

docker build --target development -t myapp:dev .

Build for production:

docker build --target production -t myapp:prod .

Language-Specific Examples

Node.js (Next.js)

# syntax=docker/dockerfile:1

# Dependencies stage
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# Builder stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Runner stage
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/public ./public
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]

Python (Django/FastAPI)

# syntax=docker/dockerfile:1

# Builder stage
FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \\
    gcc \\
    postgresql-dev \\
    && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt

# Runner stage
FROM python:3.12-slim AS runner
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \\
    postgresql-client \\
    && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/wheels /wheels
RUN pip install --no-cache /wheels/*
COPY . .
RUN useradd -m -u 1001 appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]

Go (Minimal)

# syntax=docker/dockerfile:1

# Builder stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o main .

# Runner stage (scratch = 0 bytes base)
FROM scratch
COPY --from=builder /app/main /main
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER 1001:1001
ENTRYPOINT ["/main"]

Java (Spring Boot)

# syntax=docker/dockerfile:1

# Dependencies stage
FROM eclipse-temurin:21-jdk-jammy AS deps
WORKDIR /app
COPY mvnw pom.xml ./
COPY .mvn .mvn
RUN ./mvnw dependency:go-offline

# Builder stage
FROM deps AS builder
COPY src ./src
RUN ./mvnw clean package -DskipTests

# Extractor stage (for layered JAR)
FROM builder AS extractor
WORKDIR /app
RUN java -Djarmode=layertools -jar target/*.jar extract --destination target/extracted

# Runner stage
FROM eclipse-temurin:21-jre-jammy AS runner
WORKDIR /app
RUN useradd -m -u 1001 appuser
USER appuser
COPY --from=extractor /app/target/extracted/dependencies/ ./
COPY --from=extractor /app/target/extracted/spring-boot-loader/ ./
COPY --from=extractor /app/target/extracted/snapshot-dependencies/ ./
COPY --from=extractor /app/target/extracted/application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

Advanced Techniques

Copying from External Images

# Copy from official images
FROM alpine:3.21 AS production
COPY --from=nginx:alpine /usr/share/nginx/html /usr/share/nginx/html
COPY --from=myregistry/common:latest /app/lib /app/lib

Conditional Stages (BuildKit)

# syntax=docker/dockerfile:1
ARG BUILD_ENV=production

FROM node:20 AS development
ENV NODE_ENV=development
CMD ["npm", "run", "dev"]

FROM node:20-alpine AS production
ENV NODE_ENV=production
CMD ["node", "dist/server.js"]

FROM ${BUILD_ENV} AS final
docker build --build-arg BUILD_ENV=development -t myapp:dev .
docker build --build-arg BUILD_ENV=production -t myapp:prod .

Parallel Stages

# syntax=docker/dockerfile:1

# Independent stages run in parallel
FROM alpine AS fetch-config
RUN wget https://example.com/config.json -O /config.json

FROM alpine AS fetch-data
RUN wget https://example.com/data.csv -O /data.csv

# Final stage waits for both
FROM alpine AS final
COPY --from=fetch-config /config.json /app/
COPY --from=fetch-data /data.csv /app/

Build Optimization

Target Specific Stage

# Build only up to specific stage
docker build --target builder -t myapp:builder .

# Useful for debugging
docker run -it myapp:builder /bin/sh

Cache Mounts with Multi-Stage

# syntax=docker/dockerfile:1

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \\
    go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \\
    --mount=type=cache,target=/root/.cache/go-build \\
    go build -o app

FROM alpine:3.21
COPY --from=builder /app/app /app
CMD ["/app"]

Best Practices

1. Order Stages Logically

# dependencies → builder → test → production
FROM node:20 AS deps
# Install dependencies

FROM deps AS builder
# Build application

FROM builder AS test
# Run tests

FROM node:20-alpine AS production
# Minimal runtime

2. Name Stages Clearly

# Good
FROM golang:1.21 AS compiler
FROM alpine:3.21 AS runtime

# Bad
FROM golang:1.21 AS stage1
FROM alpine:3.21 AS stage2

3. Minimize Final Stage

# Copy only what's needed
COPY --from=builder /app/binary /app/binary

# Don't copy unnecessary files
# Bad: COPY --from=builder /app /app

4. Use Specific Base Images per Stage

# JDK for building
FROM eclipse-temurin:21-jdk-jammy AS builder

# JRE for running (smaller)
FROM eclipse-temurin:21-jre-jammy AS runner

5. Document Stages

# syntax=docker/dockerfile:1

# Stage 1: Install and cache dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# Stage 2: Build application with dependencies
FROM deps AS builder
COPY . .
RUN npm run build

# Stage 3: Production runtime with minimal footprint
FROM node:20-alpine AS production
ENV NODE_ENV=production
WORKDIR /app
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/server.js"]

Common Pitfalls

1. Not Using Stage Names

# Hard to maintain
FROM node:20
COPY --from=0 /app/node_modules ./node_modules

# Better
FROM node:20 AS deps
COPY --from=deps /app/node_modules ./node_modules

2. Copying Entire Stage

# Bad - copies everything including build tools
COPY --from=builder /app /app

# Good - copy only artifacts
COPY --from=builder /app/dist /app/dist

3. Not Leveraging Cache

# Bad - copy all before installing
COPY . .
RUN npm install

# Good - install dependencies first (cached)
COPY package*.json ./
RUN npm install
COPY . .

Troubleshooting

Check Stage Sizes

# Build with specific target
docker build --target builder -t myapp:builder .
docker build --target production -t myapp:production .

# Compare sizes
docker images | grep myapp

Debug Intermediate Stages

# Build up to specific stage
docker build --target builder -t debug:builder .

# Run and inspect
docker run -it debug:builder /bin/sh

View Build Process

# Enable BuildKit with progress
DOCKER_BUILDKIT=1 docker build --progress=plain .

# See all stages and their timing

References