Multi-Stage Builds
Multi-stage builds allow you to use multiple FROM instructions in a single Dockerfile, dramatically reducing final image size by separating build-time dependencies from runtime.
The Problem Multi-Stage Solves
Without multi-stage builds, images include all build tools:
# Single-stage build - LARGE IMAGE
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install # Includes devDependencies
COPY . .
RUN npm run build # Build tools in image
CMD ["node", "dist/index.js"]
# Result: 1GB+ image with npm, build tools, source code
Multi-Stage Build Syntax
Each FROM instruction starts a new build stage:
# Stage 1: Build
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:18-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm install --only=production
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]
# Result: ~150MB image with only runtime dependencies
How Multi-Stage Works
┌─────────────────────────────────────────────────────────────┐
│ Stage 1: builder │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ node:18 (full image) │ │
│ │ + All source code │ │
│ │ + devDependencies │ │
│ │ + Build tools │ │
│ │ → Produces: /app/dist/ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ COPY --from=builder │
│ ▼ │
│ Stage 2: production │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ node:18-alpine (small image) │ │
│ │ + Only production dependencies │ │
│ │ + Built application (from Stage 1) │ │
│ │ → Final image │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Naming Stages
Name stages for clarity and reference:
# Named stages
FROM node:18 AS dependencies
FROM node:18 AS builder
FROM node:18-alpine AS production
# Copy from named stage
COPY --from=builder /app/dist ./dist
COPY --from=dependencies /app/node_modules ./node_modules
Building Specific Stages
Target specific stages during build:
# Build only the builder stage
docker build --target builder -t myapp:builder .
# Build production stage (default - last stage)
docker build -t myapp:production .
# Build dependencies stage
docker build --target dependencies -t myapp:deps .
Practical Examples
Node.js Application
# Build stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine
WORKDIR /app
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]
Go Application
Go compiles to a static binary, enabling ultra-small images:
# 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 -o main .
# Production stage - distroless for security
FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/main /
CMD ["/main"]
# Result: ~10MB image!
Python Application
# Build stage
FROM python:3.11 AS builder
WORKDIR /app
RUN pip install --user pipenv
ENV PIPENV_VENV_IN_PROJECT=1
COPY Pipfile Pipfile.lock ./
RUN pipenv install --deploy
# Production stage
FROM python:3.11-slim
WORKDIR /app
# Copy virtual environment from builder
COPY --from=builder /app/.venv ./.venv
ENV PATH="/app/.venv/bin:$PATH"
COPY . .
CMD ["python", "app.py"]
React Application with Nginx
# Build stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# Result: ~25MB image with optimized static files
Multiple Build Stages
You can have more than two stages:
# Stage 1: Base dependencies
FROM node:18 AS base
WORKDIR /app
COPY package*.json ./
# Stage 2: Development dependencies
FROM base AS dev-deps
RUN npm install
# Stage 3: Production dependencies
FROM base AS prod-deps
RUN npm install --only=production
# Stage 4: Build
FROM dev-deps AS builder
COPY . .
RUN npm run build
# Stage 5: Development image
FROM dev-deps AS development
COPY . .
CMD ["npm", "run", "dev"]
# Stage 6: Production image
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]
Build different images:
docker build --target development -t myapp:dev .
docker build --target production -t myapp:prod .
Copying from External Images
Copy from any image, not just build stages:
FROM alpine
# Copy from an external image
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/
# Copy from official tool images
COPY --from=docker:cli /usr/local/bin/docker /usr/local/bin/
COPY --from=docker/compose:latest /usr/local/bin/docker-compose /usr/local/bin/
Size Comparison
Example image sizes for a Node.js application:
| Build Type | Image Size |
|---|---|
| Single stage (node:18) | 1.1 GB |
| Multi-stage (node:18-alpine) | 150 MB |
| With production deps only | 100 MB |
| With optimized layers | 85 MB |
Best Practices
Use Appropriate Base Images
# Build stage: Full image with build tools
FROM node:18 AS builder
# Production: Minimal image
FROM node:18-alpine AS production
# or
FROM gcr.io/distroless/nodejs18-debian11 AS production
Clean Up in Build Stages
FROM node:18 AS builder
WORKDIR /app
COPY . .
RUN npm ci \
&& npm run build \
&& npm prune --production # Remove devDependencies
Use .dockerignore
# Exclude unnecessary files from build context
node_modules
.git
*.md
Dockerfile
.dockerignore
Leverage Layer Caching
FROM node:18 AS builder
WORKDIR /app
# Copy package files first (changes less often)
COPY package*.json ./
RUN npm ci
# Then copy source (changes more often)
COPY . .
RUN npm run build
Key Takeaways
- Multi-stage builds separate build-time from runtime dependencies
- Each FROM instruction starts a new stage
- COPY --from copies files between stages or from external images
- Name stages with AS for clarity
- Target specific stages with --target for development builds
- Multi-stage builds can reduce image size by 90% or more
- Use minimal base images for production stages
- Go applications can use scratch or distroless for tiny images

