This content originally appeared on DEV Community and was authored by Darshan Vasani
  
  
   What is a Multi-Stage Build in Docker?
 What is a Multi-Stage Build in Docker?
Multi-stage build allows you to use multiple
FROMstatements 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?
 Why Do We Need It?
 Main Goals:
 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
 Real-World Analogy
Imagine:
 Stage 1 = Construction site (messy, heavy tools) Stage 1 = Construction site (messy, heavy tools)
 Stage 2 = Finished house (clean, cozy) 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
 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"]
  
  
   Key Concepts Explained
 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
 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
 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;"]
 This builds the app with Node.js, and serves the static files via NGINX (no Node.js in final image!)
 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
 Common Multi-Stage Use Cases
| Use Case | Description | 
|---|---|
|  Frontend builds | Use node+nginxcombo | 
|  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
 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 COPYpaths 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
 Combine with Docker Compose
You can define multi-stage builds in your Dockerfile and just run:
docker-compose build
docker-compose up
Your services will use the optimized final image automatically 

  
  
   Example Multi-Stage for TypeScript API
 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"]
  
  
   Summary: When to Use Multi-Stage Builds?
 Summary: When to Use Multi-Stage Builds?
 Always use if:
 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
 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)
 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" ]
  
  
   A2Z Breakdown of Each Section
 A2Z Breakdown of Each Section
  
  
   
 FROM node:20-alpine3.19 as base
 What it does:
 What it does:
- Starts from a minimal Node.js 20 Alpine image
- Alpine is lightweight (~5MB), good for small, fast images
- 
as basenames this stage for reuse
Think of
baselike a shared template that both stages use.
  
  
   Stage 1: Builder
 Stage 1: Builder
FROM base as builder
WORKDIR /home/build
 What happens here:
 What happens here:
- We switch to a new build stage, using baseimage
- 
WORKDIR /home/buildsets a directory for our build process
COPY package*.json .
COPY tsconfig.json .
RUN npm install
 Install dependencies:
 Install dependencies:
- 
package*.jsoncopied to install dependencies
- 
tsconfig.jsonis required for TypeScript compilation
- 
npm installinstalls all dependencies (dev + prod)
COPY src/ src/
RUN npm run build
 Build your app:
 Build your app:
- Copies your app’s TypeScript code
- 
npm run buildcompiles TS into JS โ typically inside/dist
  
  
   End Result of Stage 1:
 End Result of Stage 1:
A folder
/home/build/distwith compiled production-ready JS output.
  
  
   Stage 2: Runner
 Stage 2: Runner
FROM base as runner
WORKDIR /home/app
 What it does:
 What it does:
- We now create a fresh container just for running the app.
- 
WORKDIR /home/appis where your app will run from.
COPY --from=builder /home/build/dist dist/
COPY --from=builder /home/build/package*.json .
 Copy built artifacts only:
 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
 Production-only install:
 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
 Add Secure Non-Root User
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nodejs
USER nodejs
 Why?
 Why?
- Running as rootis dangerous in containers 
- We create a user nodejswith limited permissions for safety
- UID/GID 1001is just an arbitrary non-root system user
  
  
   Port & Env Setup
 Port & Env Setup
EXPOSE 8000
ENV PORT=8000
- 
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
-pto map it to host:
docker run -p 8000:8000 <image>
  
  
   Start the App
 Start the App
CMD [ "npm", "start" ]
 Default entrypoint when container runs
 Default entrypoint when container runs
- This triggers your "start"script frompackage.json:
  "start": "node dist/index.js"
  
  
   Summary Table
 Summary Table
|  Section |  Purpose | 
|---|---|
| FROM base | Reuse image to reduce duplication | 
| builder | Compiles TypeScript into JS | 
| runner | Runs a minimal production image | 
| npm installin builder | Installs full deps for building | 
| npm install --omit=devin runner | Installs only what’s needed to run | 
| COPY --from=builder | Efficient file copy without rebuild | 
| USER nodejs | Enhances container security | 
  
  
   Resulting Benefits
 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
 Bonus Tip: View Image Sizes
docker images
Compare the multi-stage image (~100MB) vs a single-stage image (~400โ600MB) 
  
  
   Final Thoughts
 Final Thoughts
This approach follows Docker best practices:
- Multi-stage
- Production-ready
- Secure by default
- Reproducible builds
This content originally appeared on DEV Community and was authored by Darshan Vasani
