Skip to main content

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

BaseSizeShellUse Case
node:20~1 GBDevelopment only
node:20-slim~250 MBGood default
node:20-alpine~170 MB✓ (sh)Production, most workloads
gcr.io/distroless/nodejs20~130 MBMaximum 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 --from=builder /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 --from=builder /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 --from=builder /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 --from=builder /app/dist ./dist
COPY --from=builder /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

CheckAction
Base imageSwitch 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 .dockerignore to keep the build context small and secure.
  • Clean package caches in the same layer they are created.
  • Measure with docker images and docker history before and after optimization.

What's Next