Image Building Best Practices
Building efficient, secure, and maintainable Docker images is crucial for production-grade containerized applications. This guide covers proven best practices for creating optimized Docker images.
Use Minimal Base Images
Choosing the right base image significantly impacts your image size, security posture, and performance.
Base Image Comparison
| Base Image | Size | Packages | Use Case |
|---|---|---|---|
ubuntu:22.04 | ~77 MB | Full system | General purpose, debugging |
debian:12-slim | ~75 MB | Minimal Debian | Production workloads |
alpine:3.19 | ~7 MB | Minimal with musl | Smallest footprint |
distroless | ~2-20 MB | No shell, minimal | Maximum security |
scratch | 0 MB | Nothing | Statically compiled binaries |
Examples
# ❌ Bad: Using a full OS image
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y nodejs npm
COPY . /app
CMD ["node", "/app/server.js"]
# ✅ Good: Using a purpose-built slim image
FROM node:20-alpine
WORKDIR /app
COPY . .
CMD ["node", "server.js"]Optimize Layer Caching
Docker caches each layer of your image. Order your Dockerfile instructions from least frequently changed to most frequently changed.
Layer Caching Strategy
# ✅ Good: Dependencies change less often than source code
FROM node:20-alpine
WORKDIR /app
# Layer 1: Copy dependency definitions (changes rarely)
COPY package.json package-lock.json ./
# Layer 2: Install dependencies (cached if package files unchanged)
RUN npm ci --only=production
# Layer 3: Copy source code (changes frequently)
COPY src/ ./src/
CMD ["node", "src/index.js"]# ❌ Bad: Copying everything first invalidates dependency cache
FROM node:20-alpine
WORKDIR /app
# Any change to source code invalidates all subsequent layers
COPY . .
RUN npm ci --only=production
CMD ["node", "src/index.js"]Caching Order Principles
┌──────────────────────────────────┐
│ FROM (base image) │ ← Changes rarely
├──────────────────────────────────┤
│ System dependencies (apt/apk) │ ← Changes rarely
├──────────────────────────────────┤
│ Package manifests (copy) │ ← Changes occasionally
├──────────────────────────────────┤
│ Install dependencies (run) │ ← Changes occasionally
├──────────────────────────────────┤
│ Application source (copy) │ ← Changes frequently
├──────────────────────────────────┤
│ Build step (if applicable) │ ← Changes frequently
├──────────────────────────────────┤
│ CMD / ENTRYPOINT │ ← Changes rarely
└──────────────────────────────────┘Minimize Image Size
Combine RUN Commands
Each RUN instruction creates a new layer. Combine related commands to reduce layers:
# ❌ Bad: Multiple RUN instructions create unnecessary layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN rm -rf /var/lib/apt/lists/*
# ✅ Good: Single RUN with cleanup in the same layer
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
git && \
rm -rf /var/lib/apt/lists/*Use .dockerignore
Prevent unnecessary files from being sent to the build context:
# .dockerignore
.git
.gitignore
node_modules
npm-debug.log
Dockerfile
docker-compose.yml
.env
*.md
tests/
docs/
.vscode/
.idea/
coverage/
.nyc_output/Remove Unnecessary Files
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
# Install dependencies and clean up in one layer
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
COPY . .
CMD ["python", "app.py"]Use Multi-stage Builds
Multi-stage builds dramatically reduce final image size by separating the build environment from the runtime environment:
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .
# Stage 2: Runtime (minimal image)
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]See the Multi-stage Builds guide for more details.
Set Proper User Permissions
Never run containers as root in production:
FROM node:20-alpine
# Create a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup package.json package-lock.json ./
RUN npm ci --only=production
COPY --chown=appuser:appgroup . .
# Switch to non-root user
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]Use HEALTHCHECK
Add health checks to enable Docker to monitor container health:
FROM nginx:1.25-alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY public/ /usr/share/nginx/html/
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost/ || exit 1
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]HEALTHCHECK Parameters
| Parameter | Default | Description |
|---|---|---|
--interval | 30s | Time between health checks |
--timeout | 30s | Maximum time for a check to complete |
--start-period | 0s | Initialization time before checks start |
--retries | 3 | Consecutive failures needed to mark unhealthy |
Use Labels for Metadata
FROM node:20-alpine
LABEL maintainer="[email protected]"
LABEL org.opencontainers.image.title="My Application"
LABEL org.opencontainers.image.version="1.0.0"
LABEL org.opencontainers.image.description="A Node.js microservice"
LABEL org.opencontainers.image.source="https://github.com/org/repo"
LABEL org.opencontainers.image.licenses="MIT"
WORKDIR /app
COPY . .
RUN npm ci --only=production
CMD ["node", "server.js"]Pin Dependency Versions
Always pin specific versions for reproducible builds:
# ✅ Good: Pinned versions
FROM node:20.11.0-alpine3.19
RUN apk add --no-cache \
curl=8.5.0-r0 \
tini=0.19.0-r2
# ❌ Bad: Unpinned versions
FROM node:latest
RUN apk add --no-cache curl tiniComplete Best Practices Example
# Build stage
FROM node:20.11.0-alpine3.19 AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:20.11.0-alpine3.19
LABEL maintainer="[email protected]"
LABEL org.opencontainers.image.title="My App"
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/package.json ./
COPY --from=builder --chown=appuser:appgroup /app/package-lock.json ./
RUN npm ci --only=production && npm cache clean --force
USER appuser
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
EXPOSE 3000
CMD ["node", "dist/server.js"]Image Size Optimization Checklist
| Practice | Impact | Difficulty |
|---|---|---|
| Use Alpine/slim base images | High | Low |
| Multi-stage builds | High | Medium |
| Combine RUN commands | Medium | Low |
| Use .dockerignore | Medium | Low |
| Remove caches and temp files | Medium | Low |
| Pin dependency versions | Low (size) / High (reliability) | Low |
Use --no-install-recommends | Medium | Low |
| Use distroless images | High | Medium |
Next Steps
- Multi-stage Builds — Learn advanced multi-stage build patterns
- Build Optimization — Optimize build speed and image size
- Build Cache — Master Docker build caching
- Security Best Practices — Secure your Docker images
- Dockerfile Reference — Complete Dockerfile instruction reference