Tools: Ultimate Guide: Docker Multi-Stage Builds: Cut Your Image Size by 80%

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

Command

Copy

$ 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