DEV Community

Cover image for How I Reduced My Docker Image Size by 90% Using Multi-Stage Builds
Vipin Yadav
Vipin Yadav

Posted on

How I Reduced My Docker Image Size by 90% Using Multi-Stage Builds

Optimizing Docker Images: From Gigabytes to Megabytes

Docker images bloating to several gigabytes? Your container deployments taking forever? Multi-stage builds are the key to shrinking images, as seen in this transition from 1.2GB to 120MB.

The Problem

Standard Dockerfile builds include everything: build tools, dev dependencies, source files, and compilation artifacts. Your production container doesn't need any of that.

Before (1.2GB):

FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]

Enter fullscreen mode Exit fullscreen mode

The Solution: Multi-Stage Builds

Multi-stage builds allow the use of multiple FROM statements. Each stage can copy artifacts from previous stages while leaving behind everything else.

After (120MB):

# Stage 1: Build
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
USER node
CMD ["node", "dist/server.js"]

Enter fullscreen mode Exit fullscreen mode

Key Improvements

  1. Alpine Base Image
  2. node:18 ≈ 900MB
  3. node:18-alpine ≈ 110MB

  4. Production Dependencies Only

  5. npm ci --only=production excludes devDependencies.

  6. No TypeScript, testing frameworks, or linters in the production layer.

  7. Selective Copying

  8. Only copies dist/, node_modules/, and necessary config files.


Real-World Examples

1. Go Application

Go benefits significantly as the entire compiler is excluded from the final image.
Result: 800MB → 15MB

# Build stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .

# Production stage
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
CMD ["./main"]

Enter fullscreen mode Exit fullscreen mode

2. Python with pip

FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt

FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]

Enter fullscreen mode Exit fullscreen mode

3. Java with Maven

FROM maven:3.9-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests

FROM openjdk:17-jdk-slim
COPY --from=builder /app/target/*.jar app.jar
CMD ["java", "-jar", "app.jar"]

Enter fullscreen mode Exit fullscreen mode

Best Practices & Performance

Optimization Tips

  • Order Layers by Change Frequency: Place COPY package*.json before COPY . to leverage layer caching.
  • Use .dockerignore: Exclude node_modules, .git, and environment files.
  • Combine Commands: Use && and \ to reduce the number of layers in a single stage.

Performance Impact

Metric Impact
Build Time (Subsequent) 60% Faster
Push/Pull Time ~85-90% Faster
Disk Space 90% Reduction
Memory Usage 40% Reduction

Debugging Multi-Stage Builds

You can build or inspect specific stages using the --target flag:

# Build specific stage for debugging
docker build --target builder -t myapp:debug .

# Inspect intermediate layers
docker history myapp:builder

Enter fullscreen mode Exit fullscreen mode

Conclusion

The initial investment in refactoring your Dockerfile pays dividends in performance, cost, and developer experience. Start with your largest images first—that's where you'll see the biggest wins.

What size reduction did you achieve? Share your results below!

#docker #devops #containers #performance


Top comments (0)