DEV Community

Darshan Vasani
Darshan Vasani Subscriber

Posted on • Edited on

๐Ÿงฑ What is a Multi-Stage Build in Docker?

๐Ÿงฑ What is a Multi-Stage Build in Docker?

Multi-stage build allows you to use multiple FROM statements in a single Dockerfile to:

  • Build the app in one stage ๐Ÿ—๏ธ
  • Copy only what's needed to a smaller final image ๐Ÿ“ฆ

โ“ Why Do We Need It?

โœ… Main Goals:

๐Ÿš€ Benefit ๐Ÿ’ฌ Why it Matters
โšก Smaller Images Only copy what's needed into final image
๐Ÿ” More Secure No dev tools or secrets in production image
๐Ÿ› ๏ธ Cleaner CI/CD Separate build & runtime environment
๐Ÿ“š Better Layer Caching Speeds up builds
๐ŸŒ Environment Separation One image builds everything!

๐Ÿง  Real-World Analogy

Imagine:

  • ๐Ÿ—๏ธ Stage 1 = Construction site (messy, heavy tools)
  • ๐Ÿ  Stage 2 = Finished house (clean, cozy)

You build in the messy environment, but only move the furniture into the clean house. ๐Ÿงน


๐Ÿงช Multi-Stage Build Syntax

# ๐Ÿ”จ Stage 1: Build Stage FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build # ๐Ÿ“ฆ Stage 2: Final Production Image FROM node:20-alpine WORKDIR /app # Copy only final build artifacts (no source or node_modules) COPY --from=builder /app/dist ./dist COPY --from=builder /app/package.json ./ RUN npm ci --omit=dev # Set env vars, port and run ENV PORT=3000 EXPOSE 3000 CMD ["node", "dist/index.js"] 
Enter fullscreen mode Exit fullscreen mode

๐Ÿ” Key Concepts Explained

Keyword Meaning
AS builder Give a name to this stage
--from=builder Copy files from previous stage
npm ci --omit=dev Install only production deps
COPY . . Used only in build stage to avoid code bloat in final image

๐Ÿ“ฆ Before vs After: Image Size

Approach Image Size Contents
๐Ÿ˜ตโ€๐Ÿ’ซ Traditional Single Build ~900MB Full source code + dev dependencies
๐Ÿคฉ Multi-Stage Build ~200MB Just built app + runtime dependencies

๐Ÿ’ฅ Real Project Example: React App

# Step 1: Build React App FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # Step 2: Serve using NGINX FROM nginx:alpine COPY --from=builder /app/build /usr/share/nginx/html EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] 
Enter fullscreen mode Exit fullscreen mode

โœ… This builds the app with Node.js, and serves the static files via NGINX (no Node.js in final image!)


๐ŸŽฏ Common Multi-Stage Use Cases

Use Case Description
๐Ÿ’ป Frontend builds Use node + nginx combo
๐Ÿ”ง Backend builds Build with TS/Go/Rust, then copy binaries only
๐Ÿงช Testing stage Add test/linting in one stage, skip in final
๐Ÿ“ฆ CI/CD pipelines Clean, reproducible builds across stages

๐Ÿงฐ Pro Tips & Best Practices

๐Ÿ’ก Tip โœ… Recommendation
Use --omit=dev Strip dev-only packages in final stage
Use .dockerignore Exclude node_modules, .git, tests/, etc
Use labels Add metadata like version, author, etc
Donโ€™t copy everything Use exact COPY paths for size control
Use named stages Easier to copy from (--from=builder)
Keep final image minimal Just enough to run your app (no tools!)

๐Ÿ”„ Combine with Docker Compose

You can define multi-stage builds in your Dockerfile and just run:

docker-compose build docker-compose up 
Enter fullscreen mode Exit fullscreen mode

Your services will use the optimized final image automatically ๐Ÿง โœ…


๐Ÿ› ๏ธ Example Multi-Stage for TypeScript API

# Stage 1: Compile TS FROM node:20-alpine AS builder WORKDIR /app COPY . . RUN npm install RUN npm run build # Stage 2: Run with only JS output FROM node:20-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/package*.json ./ RUN npm ci --omit=dev CMD ["node", "dist/server.js"] 
Enter fullscreen mode Exit fullscreen mode

โœ… Summary: When to Use Multi-Stage Builds?

โœ… Always use if:

  • You're using build tools like tsc, webpack, vite
  • You want minimal production images
  • You want to separate testing/staging/building
  • You want faster CI builds & smaller attack surface

๐Ÿงพ Final TL;DR Cheatsheet

Stage Purpose Base Image Output
Stage 1 (builder) Build, compile, test node, golang, rust, etc. /dist, /build, etc.
Stage 2 (prod) Serve/run app only node:alpine, nginx, etc. Final slim image

๐Ÿ“ฆ Full Dockerfile (Context Recap)

FROM node:20-alpine3.19 as base # Stage 1: Build Stuff FROM base as builder WORKDIR /home/build COPY package*.json . COPY tsconfig.json . RUN npm install COPY src/ src/ RUN npm run build # Stage 2: Runner FROM base as runner WORKDIR /home/app COPY --from=builder /home/build/dist dist/ COPY --from=builder /home/build/package*.json . RUN npm install --omit=dev RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nodejs USER nodejs EXPOSE 8000 ENV PORT=8000 CMD [ "npm", "start" ] 
Enter fullscreen mode Exit fullscreen mode

๐ŸŽฏ A2Z Breakdown of Each Section


๐Ÿงฑ FROM node:20-alpine3.19 as base

๐Ÿง  What it does:

  • Starts from a minimal Node.js 20 Alpine image
  • Alpine is lightweight (~5MB), good for small, fast images
  • as base names this stage for reuse

๐Ÿงฉ Think of base like a shared template that both stages use.


๐Ÿ”จ Stage 1: Builder

FROM base as builder WORKDIR /home/build 
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”ง What happens here:

  • We switch to a new build stage, using base image
  • WORKDIR /home/build sets a directory for our build process

COPY package*.json . COPY tsconfig.json . RUN npm install 
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ฆ Install dependencies:

  • package*.json copied to install dependencies
  • tsconfig.json is required for TypeScript compilation
  • npm install installs all dependencies (dev + prod)

COPY src/ src/ RUN npm run build 
Enter fullscreen mode Exit fullscreen mode

๐Ÿ› ๏ธ Build your app:

  • Copies your app's TypeScript code
  • npm run build compiles TS into JS โ†’ typically inside /dist

โœ… End Result of Stage 1:

A folder /home/build/dist with compiled production-ready JS output.


๐Ÿš€ Stage 2: Runner

FROM base as runner WORKDIR /home/app 
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ What it does:

  • We now create a fresh container just for running the app.
  • WORKDIR /home/app is where your app will run from.

COPY --from=builder /home/build/dist dist/ COPY --from=builder /home/build/package*.json . 
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“‚ Copy built artifacts only:

  • Only copy the dist/ folder and package files (no source, no tsconfig)
  • Ensures the final image is slim & clean

RUN npm install --omit=dev 
Enter fullscreen mode Exit fullscreen mode

๐Ÿ” Production-only install:

  • Installs only prod dependencies (no dev tools like eslint, tsc, etc.)
  • Keeps final image light and secure โœ…

๐Ÿ‘ฎ Add Secure Non-Root User

RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nodejs USER nodejs 
Enter fullscreen mode Exit fullscreen mode

๐Ÿ” Why?

  • Running as root is dangerous in containers โŒ
  • We create a user nodejs with limited permissions for safety
  • UID/GID 1001 is just an arbitrary non-root system user

๐ŸŒ Port & Env Setup

EXPOSE 8000 ENV PORT=8000 
Enter fullscreen mode Exit fullscreen mode
  • EXPOSE 8000: Documents that the app uses port 8000
  • ENV PORT=8000: Sets the default port for app to use internally

You still need to use -p to map it to host:
docker run -p 8000:8000 <image>


๐Ÿšฆ Start the App

CMD [ "npm", "start" ] 
Enter fullscreen mode Exit fullscreen mode

๐ŸŸข Default entrypoint when container runs

  • This triggers your "start" script from package.json:
 "start": "node dist/index.js" 
Enter fullscreen mode Exit fullscreen mode

โœ… Summary Table

๐Ÿ”น Section ๐Ÿ” Purpose
FROM base Reuse image to reduce duplication
builder Compiles TypeScript into JS
runner Runs a minimal production image
npm install in builder Installs full deps for building
npm install --omit=dev in runner Installs only what's needed to run
COPY --from=builder Efficient file copy without rebuild
USER nodejs Enhances container security

๐Ÿ“Š Resulting Benefits

๐Ÿš€ Benefit โœ… Achieved
Small Image โœ… Only runtime code in final image
Secure โœ… Non-root user, no dev tools
Faster Builds โœ… Reuses build layers
Clean Code Separation โœ… No TypeScript or build files inside final container
Portable โœ… Can run on any platform with Node 20

๐Ÿง  Bonus Tip: View Image Sizes

docker images 
Enter fullscreen mode Exit fullscreen mode

Compare the multi-stage image (~100MB) vs a single-stage image (~400โ€“600MB) ๐Ÿคฏ


๐Ÿ”š Final Thoughts

This approach follows Docker best practices:

  • Multi-stage
  • Production-ready
  • Secure by default
  • Reproducible builds

Top comments (0)