Dockerfile Optimization Guide
Comprehensive guide for optimizing Docker images for size, build time, and runtime performance.
Claude Code Knowledge Pack7/10/2026
Dockerfile Optimization Guide
Comprehensive guide for optimizing Docker images for size, build time, and runtime performance.
Image Size Optimization
1. Choose Minimal Base Images
Size Comparison:
ubuntu:22.04 ~80 MB
alpine:3.21 ~5 MB
distroless/base ~20 MB
scratch ~0 MB (empty)
When to use each:
Alpine - General purpose minimal Linux
FROM alpine:3.21
RUN apk add --no-cache python3
- ✅ Very small (5 MB)
- ✅ Has package manager
- ✅ Good for interpreted languages
- ⚠️ Uses musl libc (compatibility issues with some C libraries)
Distroless - Production containers
FROM gcr.io/distroless/python3
COPY --from=builder /app /app
- ✅ No shell, package manager (secure)
- ✅ Minimal attack surface
- ✅ Small size
- ⚠️ Cannot exec into container for debugging
- ⚠️ Must use multi-stage builds
Scratch - Static binaries only
FROM scratch
COPY --from=builder /app/binary /
- ✅ Absolutely minimal
- ✅ Perfect for Go, Rust static binaries
- ⚠️ No OS utilities
- ⚠️ No debug capabilities
2. Multi-Stage Builds
Problem: Build tools bloat production images
Single-stage (bloated):
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o server
CMD ["./server"]
# Result: ~1 GB (includes Go toolchain)
Multi-stage (optimized):
# Build stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o server
# Production stage
FROM alpine:3.21
COPY --from=builder /app/server /server
CMD ["/server"]
# Result: ~10 MB (100x smaller!)
3. Layer Optimization
Combine RUN commands:
# Bad - 4 layers, poor caching
RUN apt-get update
RUN apt-get install -y curl
RUN curl -O https://example.com/file
RUN rm -f file
# Good - 1 layer, cache cleaned
RUN apt-get update && apt-get install -y --no-install-recommends \\
curl \\
&& curl -O https://example.com/file \\
&& rm -rf /var/lib/apt/lists/*
4. Package Manager Cache Cleanup
APT (Debian/Ubuntu):
RUN apt-get update && apt-get install -y --no-install-recommends \\
package1 \\
package2 \\
&& rm -rf /var/lib/apt/lists/*
- Saves ~100-200 MB per layer
- Must be in same RUN command
APK (Alpine):
RUN apk add --no-cache package1 package2
- Doesn't create cache at all
- Or:
apk add package && rm -rf /var/cache/apk/*
YUM/DNF (RHEL/Fedora):
RUN yum install -y package \\
&& yum clean all \\
&& rm -rf /var/cache/yum
Pip (Python):
RUN pip install --no-cache-dir package
NPM (Node.js):
RUN npm ci --only=production
# Or with cache mount:
RUN --mount=type=cache,target=/root/.npm \\
npm ci --only=production
5. Use .dockerignore
Problem: Entire project copied into image
.dockerignore contents:
.git/
node_modules/
*.log
.env
tests/
docs/
README.md
Impact:
- Faster builds (smaller context)
- Smaller images (fewer files)
- Prevents accidental secret leaks
Build Time Optimization
1. Leverage Build Cache
Order matters - least to most frequently changing:
# 1. Base image (rarely changes)
FROM node:21-alpine
# 2. System dependencies (rarely change)
RUN apk add --no-cache curl
# 3. Application dependencies (change occasionally)
COPY package*.json ./
RUN npm ci
# 4. Application code (changes frequently)
COPY . .
RUN npm run build
Why this works:
- Docker caches each layer
- Layers rebuild when files change
- Putting frequently-changing files last preserves cache for earlier layers
2. BuildKit Cache Mounts
Enable BuildKit:
Use cache mounts:
# syntax=docker/dockerfile:1
# Python with pip cache
RUN --mount=type=cache,target=/root/.cache/pip \\
pip install -r requirements.txt
# Node.js with npm cache
RUN --mount=type=cache,target=/root/.npm \\
npm ci
# Go with module cache
RUN --mount=type=cache,target=/go/pkg/mod \\
go build -o app
Benefits:
- Persistent cache across builds
- Dramatically faster dependency installation
- Shared cache between projects
3. Parallel Multi-Stage Builds
# These stages run in parallel
FROM alpine AS fetch-1
RUN wget https://example.com/file1
FROM alpine AS fetch-2
RUN wget https://example.com/file2
# This stage waits for both
FROM alpine
COPY --from=fetch-1 /file1 .
COPY --from=fetch-2 /file2 .
Runtime Performance Optimization
1. Exec Form for CMD/ENTRYPOINT
# Bad - shell form (extra shell process)
CMD python app.py
# Good - exec form (direct execution)
CMD ["python", "app.py"]
Benefits:
- Faster startup (no shell)
- Proper signal handling (SIGTERM)
- Lower memory usage
2. Health Checks
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \\
CMD curl -f http://localhost:8080/health || exit 1
Benefits:
- Container orchestrators can detect unhealthy containers
- Automatic restarts
- Better uptime
3. Resource Awareness
# Use all available CPUs
ENV GOMAXPROCS=0
# Or limit to specific count
ENV GOMAXPROCS=4
Language-Specific Optimizations
Node.js
FROM node:21-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:21-alpine
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER node
CMD ["node", "server.js"]
Tips:
- Use
npm ciinstead ofnpm install - Install only production dependencies
- Use Alpine variant (node:21-alpine vs node:21 = 150MB vs 900MB)
Python
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM python:3.12-slim
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
COPY . .
USER nobody
CMD ["python", "app.py"]
Tips:
- Use slim variant (python:3.12-slim vs python:3.12 = 50MB vs 1GB)
- Install to --user to copy to final stage
- Use --no-cache-dir to avoid pip cache
Go
FROM golang:1.21-alpine AS builder
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app
FROM scratch
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
Tips:
- Use scratch for static binaries
- Disable CGO for static linking
- Use
-ldflags="-s -w"to strip debug info (smaller binary)
Java
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package
FROM eclipse-temurin:21-jre-alpine
COPY --from=builder /app/target/*.jar /app.jar
CMD ["java", "-jar", "/app.jar"]
Tips:
- Use JRE instead of JDK for runtime (smaller)
- Download dependencies separately for caching
- Consider custom JRE with jlink for minimal image
Advanced Techniques
1. Multi-Architecture Builds
docker buildx build --platform linux/amd64,linux/arm64 -t myapp .
2. Build Secrets
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \\
npm ci
docker build --secret id=npmrc,src=$HOME/.npmrc .
Benefits:
- Secrets not in final image
- Not in build history
- Secure credential usage
3. SSH Mounts
RUN --mount=type=ssh \\
git clone git@github.com:private/repo.git
docker build --ssh default .
4. Layer Squashing
docker build --squash -t myapp .
Benefits:
- Single layer in final image
- Smaller size if cleanup commands are separate
Drawbacks:
- Loses layer caching benefits
- Slower rebuilds
Optimization Checklist
- Use minimal base image (Alpine, distroless, scratch)
- Implement multi-stage builds
- Combine RUN commands
- Clean package manager cache
- Order layers by change frequency
- Use BuildKit cache mounts
- Create .dockerignore file
- Use exec form for CMD/ENTRYPOINT
- Add HEALTHCHECK for services
- Pin dependency versions
- Remove development dependencies
- Use --no-install-recommends for apt
- Consider language-specific optimizations
- Enable BuildKit features
Measuring Optimization
Before Optimization
docker images myapp
# REPOSITORY TAG SIZE
# myapp latest 1.2GB
After Optimization
docker images myapp-optimized
# REPOSITORY TAG SIZE
# myapp-optimized latest 50MB
Build Time Comparison
time docker build -t myapp .
# real 5m30s
time docker build -t myapp-optimized .
# real 0m45s (with cache)
Tools for Analysis
dive - Layer Analysis
dive myapp:latest
- Shows layer-by-layer size
- Identifies wasted space
- Suggests optimizations
docker history
docker history myapp:latest
- Shows each layer's size
- Identifies large layers
docker scout
docker scout cves myapp:latest
- Scans for vulnerabilities
- Recommends base image updates