Docker Multi-Stage Builds
What is a Multi-Stage Build?
A multi-stage build uses multiple FROM statements in a single Dockerfile. Each FROM starts a new build stage. You can copy files from one stage to another, leaving behind everything you don't need in the final image.
The result: Smaller, leaner production images.
The Problem Without Multi-Stage Builds
When you build a Node.js or Go application, you need:
- Build tools (compiler, npm, make)
- Source code
- Test files
- Development dependencies
But in production, you only need:
- The compiled/built output
- Runtime libraries
# Without multi-stage - EVERYTHING ends up in the image
FROM node:20
WORKDIR /app
COPY . .
RUN npm install # includes dev dependencies (huge!)
RUN npm run build
CMD ["node", "dist/server.js"]
Problem: The image includes node_modules (with dev deps), source code, build tools - it can be 1 GB+.
Multi-Stage Build Solution
# Stage 1: Build (we call it "builder")
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # install all dependencies
COPY . .
RUN npm run build # compile to /app/dist
# Stage 2: Production (starts fresh, tiny image)
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production # production dependencies only
COPY --from=builder /app/dist ./dist # copy ONLY the built output
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Result: The final image only has Alpine Linux + production node_modules + built output = ~150 MB instead of ~1 GB.
Key Syntax
Naming a Stage
FROM ubuntu:22.04 AS my-stage-name
Use AS <name> to give a stage a name. Names are lowercase.
Copying From a Previous Stage
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/build /build
Copying From an External Image
# Copy a specific file from an official image
COPY --from=nginx:alpine /etc/nginx/nginx.conf /default-nginx.conf
Building Specific Stages
# Build the full Dockerfile (default - builds the last stage)
docker build -t my-app .
# Build only up to a specific stage (useful for debugging)
docker build --target builder -t my-app-debug .
docker build --target production -t my-app .
Real-World Examples
Example 1 - Node.js App
# Stage 1: Install and Build
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
# Only production deps
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Copy built output from builder stage
COPY --from=builder /app/dist ./dist
# Non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
Example 2 - Go Application (Produces a Single Binary)
# Stage 1: Build
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/server
# Stage 2: Minimal Runtime
FROM scratch AS production
# "scratch" = completely empty image - just the binary
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]
Result: A Docker image that is just the Go binary - often under 15 MB.
Example 3 - Python App
# Stage 1: Build dependencies
FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Stage 2: Production
FROM python:3.11-slim AS production
WORKDIR /app
# Copy installed packages from builder
COPY --from=builder /root/.local /root/.local
COPY . .
# Non-root user
RUN useradd -m appuser
USER appuser
ENV PATH=/root/.local/bin:$PATH
EXPOSE 5000
CMD ["python", "app.py"]
Example 4 - React Frontend
# Stage 1: Build React App
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build # creates /app/build/ or /app/dist/
# Stage 2: Serve with Nginx
FROM nginx:alpine AS production
# Remove default nginx page
RUN rm -rf /usr/share/nginx/html/*
# Copy React build output
COPY --from=builder /app/build /usr/share/nginx/html
# Custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Example 5 - Three Stages (Test + Build + Production)
# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Stage 2: Test
FROM deps AS tester
COPY . .
RUN npm run lint
RUN npm run test
# Stage 3: Production Build
FROM deps AS builder
COPY . .
RUN npm run build
# Stage 4: Final Production Image
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Build only tests (skip production build):
docker build --target tester -t my-app-test .
Build production (skips tests if you want):
docker build --target production -t my-app .
Build Cache with Multi-Stage Builds
Docker caches each layer. Order your instructions to maximize cache hits.
# GOOD - dependencies cached separately from code
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./ # <- cache this layer
RUN npm ci # <- only re-runs if package.json changes
COPY . . # <- code changes often - put after npm install
RUN npm run build
# BAD - any code change invalidates the npm install cache
FROM node:20 AS builder
WORKDIR /app
COPY . . # <- code change here...
RUN npm ci # <- ...forces npm install to re-run every time
RUN npm run build
Size Comparison
| Approach | Image Size |
|---|---|
| Single stage with node:20 | ~1.1 GB |
| Multi-stage with node:20-alpine | ~200 MB |
| Multi-stage with scratch (Go binary) | ~10-20 MB |
Commands for Multi-Stage Builds
# Build final image (default - last stage)
docker build -t my-app .
# Build a specific stage
docker build --target builder -t my-app-build .
docker build --target production -t my-app .
# Build without cache (force fresh build)
docker build --no-cache -t my-app .
# Build with verbose output (see each step)
docker build --progress=plain -t my-app .
# Build and inspect size
docker build -t my-app . && docker images my-app
# Build with build args
docker build --build-arg VERSION=1.0 -t my-app .
Multi-Stage Builds Quick Summary
| Feature | What it does |
|---|---|
Multiple FROM | Starts a new build stage |
AS <name> | Names a stage for reference |
COPY --from=<name> | Copies files from a previous stage |
--target <name> | Builds only up to the named stage |
FROM scratch | Empty base - for single-binary apps |
Key benefit: Only the last FROM stage's files end up in the final image. Everything in earlier stages is discarded - making production images much smaller and more secure.
-> You have completed the Docker series! Review README.md for the full learning path.
FAQ
Should I memorize every Docker command?+
No. Memorize the core workflow first: build, run, list, inspect, logs, exec, stop, remove, and clean up. Then learn specialized commands when you need them.
Is Docker only for developers?+
No. Docker is useful for system administrators, infrastructure engineers, DevOps engineers, cloud engineers, support engineers, and learners who want repeatable labs.
What should I do after reading this guide?+
Run the examples, write down what each command changes, rebuild the workflow with Docker Compose, and then add one CI/CD step that builds the image automatically.
Need help applying Docker in a real project?
Work directly with Muhammad Irfan Aslam for Docker, Linux, DevOps, CI/CD, cloud deployment, or infrastructure troubleshooting support.
Hire Me for Support