Docker Multi-Stage Builds: Cut Your Image Size by 80%
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.
Why Images Get So Big
A typical Node.js Dockerfile:
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install # Installs devDependencies too
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]
Problems:
-
node:20base image is ~900MB -
npm installincludes all devDependencies (TypeScript, webpack, etc.) - Source files, test files, build tools all end up in the final image
The Multi-Stage Solution
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # Install everything including devDeps
COPY . .
RUN 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 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"]
Result: 800MB → ~95MB
Python Multi-Stage Example
# Build stage
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip 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"]
python:3.12-slim is 45MB vs python:3.12 at 900MB+.
Go Multi-Stage Example
Go is the best case for multi-stage builds — you can produce a static binary with zero runtime dependencies:
# 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"]
A Go app in a scratch image can be as small as 5-10MB total.
How to Verify Size Reduction
# Build and compare
docker build -t myapp:single-stage -f Dockerfile.old .
docker build -t myapp:multi-stage -f Dockerfile.new .
docker images | grep myapp
# myapp single-stage ... 847MB
# myapp multi-stage ... 94MB
Layer Caching Best Practices
# Order by change frequency (least-changed first)
COPY package*.json ./ # Changes rarely
RUN npm ci # Cached until package.json changes
COPY src/ ./src/ # Changes often — at the end
RUN npm run build
# Inspect what's in your image
docker history myapp:multi-stage
docker run --rm myapp:multi-stage du -sh /app
I built ARIA to solve exactly this.
Try it free at step2dev.com — no credit card needed.
Top comments (0)