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)