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"]
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"]
Key Improvements
- Alpine Base Image
-
node:18≈ 900MB node:18-alpine≈ 110MBProduction Dependencies Only
npm ci --only=productionexcludesdevDependencies.No TypeScript, testing frameworks, or linters in the production layer.
Selective Copying
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"]
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"]
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"]
Best Practices & Performance
Optimization Tips
-
Order Layers by Change Frequency: Place
COPY package*.jsonbeforeCOPY .to leverage layer caching. -
Use
.dockerignore: Excludenode_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
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)