Image Size Optimization
Large images mean slower pulls, more registry storage, and a bigger attack surface. This lesson covers practical techniques to reduce image size without breaking your application.
Impact of Image Size
flowchart LR
Large["1 GB image"] -->|"3-5 min pull"| Slow["Slow deploys"]
Large -->|"400+ packages"| Risk["More CVEs"]
Large -->|"$$ storage"| Cost["Higher cost"]
Small["150 MB image"] -->|"30s pull"| Fast["Fast deploys"]
Small -->|"~30 packages"| Safe["Fewer CVEs"]
Small -->|"$ storage"| Cheap["Lower cost"]
style Large fill:#ffebee,stroke:#c62828
style Small fill:#e8f5e9,stroke:#2e7d32
Technique 1: Choose a Minimal Base Image
| Base | Size | Shell | Use Case |
|---|---|---|---|
node:20 | ~1 GB | ✓ | Development only |
node:20-slim | ~250 MB | ✓ | Good default |
node:20-alpine | ~170 MB | ✓ (sh) | Production, most workloads |
gcr.io/distroless/nodejs20 | ~130 MB | ✗ | Maximum security, no debug shell |
Switch your FROM line and test -- most apps work on Alpine without changes.
Technique 2: Multi-Stage Builds
Keep build tools out of the final image:
Node.js
# Stage 1: Build (has npm, dev dependencies)
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Runtime (production deps only)
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]
Go (Binary-Only Image)
FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app .
FROM alpine:3.20
COPY /app /usr/local/bin/app
USER 1000
ENTRYPOINT ["/usr/local/bin/app"]
Python
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY /install /usr/local
COPY . .
USER 1000
CMD ["python", "app.py"]
Technique 3: Use .dockerignore
Exclude unnecessary files from the build context:
.git
node_modules
*.md
.env
docker-compose*.yml
__pycache__
.pytest_cache
This reduces context transfer time and prevents accidentally copying secrets or large files into the image.
Technique 4: Clean Up in the Same Layer
Package manager caches persist in layers. Clean up in the same RUN statement:
# BAD: cache stored in layer 1, removal in layer 2 doesn't help
RUN apt-get install -y curl
RUN apt-get clean
# GOOD: cache removed in the same layer
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
Technique 5: Copy Only What You Need
# BAD: copies everything (tests, docs, configs)
COPY . .
# GOOD: copy only runtime files
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
Measuring Image Size
# Check image size
docker images my-app
# See what each layer contributes
docker history my-app:1.0.0
# Full layer history (no truncation)
docker history --no-trunc my-app:1.0.0
# Detailed breakdown
docker image inspect my-app:1.0.0 --format '{{.Size}}'
For deep analysis, use dive to interactively explore layers:
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive my-app:1.0.0
Size Optimization Checklist
| Check | Action |
|---|---|
| Base image | Switch to -alpine or -slim |
| Build tools in final image? | Use multi-stage build |
| Dev dependencies included? | Install only production deps |
| Package cache left behind? | Clean in same RUN layer |
| Build context too large? | Update .dockerignore |
| Unnecessary files copied? | Use specific COPY targets |
Key Takeaways
- Multi-stage builds are the single biggest lever for image size reduction.
- Switch to Alpine or slim bases -- most apps work without changes.
- Use
.dockerignoreto keep the build context small and secure. - Clean package caches in the same layer they are created.
- Measure with
docker imagesanddocker historybefore and after optimization.
What's Next
- Continue to Build Speed Optimization.