Build Optimization
Optimizing Docker builds reduces build time, image size, and resource consumption. This guide covers practical techniques for faster, smaller, and more efficient builds.
Build Time Optimization
1. Optimize Layer Ordering
Place instructions that change rarely at the top and frequently changing instructions at the bottom:
dockerfile
# ✅ Optimized: Dependencies change less often than source code
FROM node:20-alpine
WORKDIR /app
# 1. System deps (rarely change)
RUN apk add --no-cache curl
# 2. Copy dependency manifests (change occasionally)
COPY package.json package-lock.json ./
# 3. Install deps (cached when manifests unchanged)
RUN npm ci --only=production
# 4. Copy source (changes frequently)
COPY . .
# 5. Build
RUN npm run build
CMD ["node", "dist/server.js"]2. Use Cache Mounts
Cache mounts persist package manager caches between builds:
dockerfile
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
# npm cache persists between builds — 2-5x faster installs
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production
COPY . .
CMD ["node", "server.js"]Cache Mount Speedup by Language
| Language | Package Manager | Cache Target | Typical Speedup |
|---|---|---|---|
| Node.js | npm | /root/.npm | 2-5x |
| Node.js | yarn | /usr/local/share/.cache/yarn | 2-5x |
| Node.js | pnpm | /root/.local/share/pnpm/store | 3-6x |
| Python | pip | /root/.cache/pip | 2-4x |
| Go | modules | /go/pkg/mod | 3-5x |
| Rust | cargo | /usr/local/cargo/registry | 5-10x |
| Java | Maven | /root/.m2/repository | 3-5x |
| Java | Gradle | /root/.gradle/caches | 3-5x |
| Ruby | bundler | /usr/local/bundle/cache | 2-4x |
3. Use Multi-stage Builds
Separate build tools from the runtime:
dockerfile
# Build stage: includes all build tools
FROM node:20 AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage: minimal runtime
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]4. Leverage Parallel Stage Execution
BuildKit executes independent stages concurrently:
dockerfile
# These three stages build in parallel
FROM node:20-alpine AS frontend
WORKDIR /app
COPY frontend/ .
RUN npm ci && npm run build
FROM golang:1.22-alpine AS api
WORKDIR /app
COPY api/ .
RUN go build -o server .
FROM python:3.12-slim AS worker
WORKDIR /app
COPY worker/ .
RUN pip install -r requirements.txt
# Final stage combines all
FROM alpine:3.19
COPY --from=frontend /app/dist /srv/public
COPY --from=api /app/server /srv/api
COPY --from=worker /app /srv/worker5. Minimize Build Context
Use .dockerignore to reduce the files sent to the builder:
# .dockerignore
.git
node_modules
dist
build
*.md
tests/
coverage/
.env*
docker-compose*.ymlbash
# Check build context size
du -sh --exclude=.git .
# A large context significantly slows builds
# Target: < 10 MB for most projectsImage Size Optimization
1. Choose Minimal Base Images
| Base Image | Size | Use Case |
|---|---|---|
scratch | 0 MB | Static binaries (Go, Rust) |
alpine:3.19 | ~7 MB | General purpose minimal |
distroless/static | ~2 MB | Static binaries with CA certs |
distroless/base | ~20 MB | Dynamic binaries (C/C++) |
distroless/nodejs20 | ~130 MB | Node.js apps |
node:20-alpine | ~130 MB | Node.js with shell |
node:20-slim | ~200 MB | Node.js Debian slim |
node:20 | ~1 GB | Full Node.js Debian |
2. Combine and Clean in Single Layer
dockerfile
# ❌ Bad: Multiple layers, cache not cleaned
RUN apt-get update
RUN apt-get install -y curl git
RUN apt-get clean
# ✅ Good: Single layer with cleanup
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*3. Remove Unnecessary Files
dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt && \
find /usr/local/lib/python3.12 -type d -name __pycache__ -exec rm -rf {} + && \
find /usr/local/lib/python3.12 -type f -name '*.pyc' -delete && \
find /usr/local/lib/python3.12 -type f -name '*.pyo' -delete
COPY . .
CMD ["python", "app.py"]4. Use Static Binaries with Scratch
dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o server .
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]Result: Final image is typically 3-10 MB.
5. Production Dependencies Only
dockerfile
# Node.js: Install only production dependencies
RUN npm ci --only=production
# Python: Exclude dev dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Go: Build with optimizations
RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o app .
# Rust: Release build
RUN cargo build --releaseBuild Performance Monitoring
Analyze Build Time
bash
# Show detailed build timing
docker build --progress=plain -t my-app:1.0 . 2>&1 | tee build.log
# Use BuildKit's built-in profiling
BUILDKIT_PROGRESS=plain docker build -t my-app:1.0 .Analyze Image Size
bash
# View image size
docker images my-app
# View layer sizes
docker history my-app:1.0
# Detailed layer analysis with dive
dive my-app:1.0
# Check for wasted space
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive my-app:1.0Image Size Analysis Tools
| Tool | Description | Usage |
|---|---|---|
docker history | Built-in layer analysis | docker history my-app:1.0 |
dive | Interactive layer explorer | dive my-app:1.0 |
docker scout | Vulnerability + size analysis | docker scout quickview my-app:1.0 |
slim | Automatic image minification | slim build my-app:1.0 |
Dockerfile Best Practices Checklist
| Optimization | Category | Impact |
|---|---|---|
Use .dockerignore | Build time | ⭐⭐⭐ |
| Order layers by change frequency | Build time | ⭐⭐⭐ |
| Use cache mounts | Build time | ⭐⭐⭐ |
| Use multi-stage builds | Image size | ⭐⭐⭐ |
| Use minimal base images | Image size | ⭐⭐⭐ |
| Combine RUN commands | Image size | ⭐⭐ |
| Clean package manager caches | Image size | ⭐⭐ |
Use --no-install-recommends | Image size | ⭐⭐ |
| Pin dependency versions | Reliability | ⭐⭐ |
| Production dependencies only | Image size | ⭐⭐ |
| Parallel build stages | Build time | ⭐⭐ |
| External build cache | Build time | ⭐⭐ |
Complete Optimized Example
dockerfile
# syntax=docker/dockerfile:1
# ============================================
# Stage 1: Dependencies
# ============================================
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production
# ============================================
# Stage 2: Build
# ============================================
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run build
# ============================================
# Stage 3: Production
# ============================================
FROM node:20-alpine
LABEL org.opencontainers.image.title="My Optimized App"
LABEL org.opencontainers.image.version="1.0.0"
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=deps --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --chown=appuser:appgroup package.json ./
USER appuser
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD wget -q --spider http://localhost:3000/health || exit 1
EXPOSE 3000
CMD ["node", "dist/server.js"]Next Steps
- Build Cache — Deep dive into cache strategies
- BuildKit Guide — Advanced BuildKit features
- Multi-stage Builds — Multi-stage build patterns
- Image Building Best Practices — Comprehensive image best practices
- Docker Build Overview — Build fundamentals