DEV Community

Yash
Yash

Posted on

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

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"]
Enter fullscreen mode Exit fullscreen mode

Problems:

  • node:20 base image is ~900MB
  • npm install includes 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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# Inspect what's in your image
docker history myapp:multi-stage
docker run --rm myapp:multi-stage du -sh /app
Enter fullscreen mode Exit fullscreen mode

I built ARIA to solve exactly this.
Try it free at step2dev.com — no credit card needed.

Top comments (0)