Tools: Ultimate Guide: Docker Multi-Stage Builds: Cut Your Image Size by 80%
Docker Multi-Stage Builds: Cut Your Image Size by 80%
Why Images Get So Big
The Multi-Stage Solution
Python Multi-Stage Example
Go Multi-Stage Example
How to Verify Size Reduction
Layer Caching Best Practices A Node.js app with a standard Dockerfile can easily produce an 800MB+ image. The same app with a multi-stage build: 80-120MB. Here's how it works and how to implement it. A typical Node.js Dockerfile: Result: 800MB → ~95MB python:3.12-slim is 45MB vs python:3.12 at 900MB+. Go is the best case for multi-stage builds — you can produce a static binary with zero runtime dependencies: A Go app in a scratch image can be as small as 5-10MB total. I built ARIA to solve exactly this.
Try it free at step2dev.com — no credit card needed. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse
$ FROM node:20
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm -weight: 500;">install # Installs devDependencies too
COPY . .
RUN -weight: 500;">npm run build
CMD ["node", "dist/server.js"]
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm -weight: 500;">install # Installs devDependencies too
COPY . .
RUN -weight: 500;">npm run build
CMD ["node", "dist/server.js"]
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm -weight: 500;">install # Installs devDependencies too
COPY . .
RUN -weight: 500;">npm run build
CMD ["node", "dist/server.js"]
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm ci # Install everything including devDeps
COPY . .
RUN -weight: 500;">npm run build # Compile TypeScript, etc. # Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
ENV NODE_ENV=production # Only copy what's needed for production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN -weight: 500;">npm ci --only=production # Only production deps
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser EXPOSE 3000
CMD ["node", "dist/server.js"]
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm ci # Install everything including devDeps
COPY . .
RUN -weight: 500;">npm run build # Compile TypeScript, etc. # Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
ENV NODE_ENV=production # Only copy what's needed for production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN -weight: 500;">npm ci --only=production # Only production deps
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser EXPOSE 3000
CMD ["node", "dist/server.js"]
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN -weight: 500;">npm ci # Install everything including devDeps
COPY . .
RUN -weight: 500;">npm run build # Compile TypeScript, etc. # Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
ENV NODE_ENV=production # Only copy what's needed for production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN -weight: 500;">npm ci --only=production # Only production deps
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser EXPOSE 3000
CMD ["node", "dist/server.js"]
# Build stage
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN -weight: 500;">pip -weight: 500;">install --user -r requirements.txt # Production stage
FROM python:3.12-slim AS production
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "main.py"]
# Build stage
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN -weight: 500;">pip -weight: 500;">install --user -r requirements.txt # Production stage
FROM python:3.12-slim AS production
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "main.py"]
# Build stage
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN -weight: 500;">pip -weight: 500;">install --user -r requirements.txt # Production stage
FROM python:3.12-slim AS production
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "main.py"]
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server . # Final stage — scratch image (ZERO MB base)
FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server . # Final stage — scratch image (ZERO MB base)
FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server . # Final stage — scratch image (ZERO MB base)
FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]
# Build and compare
-weight: 500;">docker build -t myapp:single-stage -f Dockerfile.old .
-weight: 500;">docker build -t myapp:multi-stage -f Dockerfile.new . -weight: 500;">docker images | grep myapp
# myapp single-stage ... 847MB
# myapp multi-stage ... 94MB
# Build and compare
-weight: 500;">docker build -t myapp:single-stage -f Dockerfile.old .
-weight: 500;">docker build -t myapp:multi-stage -f Dockerfile.new . -weight: 500;">docker images | grep myapp
# myapp single-stage ... 847MB
# myapp multi-stage ... 94MB
# Build and compare
-weight: 500;">docker build -t myapp:single-stage -f Dockerfile.old .
-weight: 500;">docker build -t myapp:multi-stage -f Dockerfile.new . -weight: 500;">docker images | grep myapp
# myapp single-stage ... 847MB
# myapp multi-stage ... 94MB
# Order by change frequency (least-changed first)
COPY package*.json ./ # Changes rarely
RUN -weight: 500;">npm ci # Cached until package.json changes
COPY src/ ./src/ # Changes often — at the end
RUN -weight: 500;">npm run build
# Order by change frequency (least-changed first)
COPY package*.json ./ # Changes rarely
RUN -weight: 500;">npm ci # Cached until package.json changes
COPY src/ ./src/ # Changes often — at the end
RUN -weight: 500;">npm run build
# Order by change frequency (least-changed first)
COPY package*.json ./ # Changes rarely
RUN -weight: 500;">npm ci # Cached until package.json changes
COPY src/ ./src/ # Changes often — at the end
RUN -weight: 500;">npm run build
# Inspect what's in your image
-weight: 500;">docker history myapp:multi-stage
-weight: 500;">docker run --rm myapp:multi-stage du -sh /app
# Inspect what's in your image
-weight: 500;">docker history myapp:multi-stage
-weight: 500;">docker run --rm myapp:multi-stage du -sh /app
# Inspect what's in your image
-weight: 500;">docker history myapp:multi-stage
-weight: 500;">docker run --rm myapp:multi-stage du -sh /app - node:20 base image is ~900MB
- -weight: 500;">npm -weight: 500;">install includes all devDependencies (TypeScript, webpack, etc.)
- Source files, test files, build tools all end up in the final image