Docker

Docker Multi-Stage Builds: A Step-by-Step Tutorial

Learn how to shrink Docker images and speed up builds using multi-stage builds, with a complete Node.js example.

Marcus LeeMay 14, 20262 min read
Share:

If your Docker images are bloated with build tools that have no business being in production, multi-stage builds fix that in a few lines.

The problem

A naive Dockerfile for a Node.js app often looks like this:

dockerfile
FROM node:22
WORKDIR /app
COPY . .
RUN npm install && npm run build
CMD ["node", "dist/server.js"]

This image ships npm, dev dependencies, source maps, and your entire node_modules — often 800MB+ for what should be a 100MB runtime image.

Step 1: Split the build and runtime stages

dockerfile
# --- Stage 1: build ---
FROM node:22 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
# --- Stage 2: runtime ---
FROM node:22-slim AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
CMD ["node", "dist/server.js"]

Only the builder stage installs full dev dependencies and runs the build. The runner stage copies just the compiled output.

Step 2: Prune dependencies before copying

Go further by installing only production dependencies in a dedicated stage:

dockerfile
FROM node:22 AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
 
FROM node:22 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM node:22-slim AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/server.js"]

Step 3: Measure the difference

bash
docker build -t myapp:multistage .
docker images myapp:multistage

It's common to see a 60-80% size reduction, which directly improves pull times, cold starts, and CI cache efficiency.

Key takeaways

  • Name your stages (AS builder, AS runner) for readability and targeted --target builds.
  • Only copy what the runtime actually needs — build tools, source files, and caches should never leave the builder stage.
  • Use a -slim or -alpine base for the final stage where possible.
Advertisement

Frequently Asked Questions

Marcus Lee
Marcus Lee

Cloud & DevOps Engineer

Marcus covers Kubernetes, cloud infrastructure, and CI/CD. He's spent a decade running production systems at scale.

Related Articles