Docker image optimization patterns including multi-stage builds, layer caching, security hardening, and size reduction techniques...
Comprehensive guide to optimizing Docker images for size, build speed, and security. Covers multi-stage builds, layer caching strategies, security hardening, and production deployment patterns.
When to use this skill:
Common triggers:
Typical single-stage Dockerfile (800MB+ image):
FROM python:3.11
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
Problems:
Optimized multi-stage Dockerfile (120MB image):
# Stage 1: Builder
FROM python:3.11-slim AS builder
WORKDIR /app
# Install build dependencies in separate layer
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy only requirements first (cache optimization)
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Stage 2: Runtime
FROM python:3.11-slim
WORKDIR /app
# Copy only Python packages from builder
COPY --from=builder /root/.local /root/.local
# Copy only application code
COPY app.py .
COPY src/ ./src/
# Make sure scripts in .local are usable
ENV PATH=/root/.local/bin:$PATH
# Run as non-root user
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
CMD ["python", "app.py"]
Result: 800MB โ 120MB (85% reduction)
Stage 1: Builder (Throw away after build)
Stage 2: Runtime (Final image)
Each instruction creates a layer. Docker caches unchanged layers.
Bad Order (cache invalidated on every code change):
FROM python:3.11-slim
COPY . . # โ Copies everything
RUN pip install -r requirements.txt # โ Runs on every code change
Good Order (cache preserved):
FROM python:3.11-slim
COPY requirements.txt . # โ
Only requirements
RUN pip install -r requirements.txt # โ
Cached if requirements unchanged
COPY . . # โ
Code changes don't invalidate pip cache
1. Order by change frequency (least to most):
# 1. System dependencies (rarely change)
RUN apt-get update && apt-get install -y curl
# 2. Language runtime (rarely changes)
FROM python:3.11-slim
# 3. Dependencies (change occasionally)
COPY requirements.txt .
RUN pip install -r requirements.txt
# 4. Application code (changes frequently)
COPY . .
2. Separate COPY operations:
# โ Bad: Invalidates cache on any file change
COPY . .
# โ
Good: Cache preserved unless specific files change
COPY package.json package-lock.json ./
RUN npm ci
COPY src/ ./src/
COPY public/ ./public/
3. Use .dockerignore:
# .dockerignore
.git
.gitignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
.env
.venv
__pycache__
*.pyc
tests/
docs/
Image Size Comparison:
python:3.11 โ 1.01GB
python:3.11-slim โ 130MB (87% smaller)
python:3.11-alpine โ 50MB (95% smaller)
When to use each:
python:3.11): Never for productionpython:3.11-slim): Default choice, good compatibilitypython:3.11-alpine): Smallest, but can have glibc issuesNode.js Example (900MB โ 150MB):
# Builder stage
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Production stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Result: 900MB โ 150MB (83% reduction)
โ Bad (creates large intermediate layers):
RUN apt-get update
RUN apt-get install -y build-essential
RUN rm -rf /var/lib/apt/lists/*
โ Good (single layer, no intermediate garbage):
RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential && \
rm -rf /var/lib/apt/lists/*
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
g++ \
make \
&& pip install --no-cache-dir -r requirements.txt \
&& apt-get purge -y --auto-remove \
gcc \
g++ \
make \
&& rm -rf /var/lib/apt/lists/*
โ Bad (runs as root):
FROM python:3.11-slim
COPY app.py .
CMD ["python", "app.py"]
โ Good (runs as non-root user):
FROM python:3.11-slim
# Create non-root user
RUN useradd -m -u 1000 appuser && \
mkdir -p /app && \
chown -R appuser:appuser /app
WORKDIR /app
USER appuser
COPY --chown=appuser:appuser app.py .
CMD ["python", "app.py"]
โ Bad (secrets baked into image):
ENV DATABASE_PASSWORD=secret123
COPY .env .
โ Good (secrets provided at runtime):
# Pass secrets via environment variables at runtime
# docker run -e DATABASE_PASSWORD=$DB_PASS myapp
โ Also Good (Docker secrets):
# Use Docker secrets (Swarm/Kubernetes)
CMD ["sh", "-c", "python app.py"]
# Secrets mounted at /run/secrets/
# Using Docker Scout
docker scout cves myapp:latest
# Using Trivy
trivy image myapp:latest
# Using Snyk
snyk container test myapp:latest
โ Bad (unpredictable):
FROM python:latest
โ Good (reproducible):
FROM python:3.11.9-slim-bookworm
Enable BuildKit for faster builds:
export DOCKER_BUILDKIT=1
docker build -t myapp .
Benefits:
With BuildKit (cache pip downloads):
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
Benefits:
BuildKit automatically parallelizes independent stages:
# Stage 1: Frontend build (runs in parallel)
FROM node:20 AS frontend
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# Stage 2: Backend build (runs in parallel)
FROM python:3.11-slim AS backend
WORKDIR /app/backend
COPY backend/requirements.txt .
RUN pip install -r requirements.txt
# Stage 3: Final image (waits for both stages)
FROM python:3.11-slim
COPY --from=frontend /app/frontend/dist /app/static
COPY --from=backend /app/backend /app
FROM python:3.11-slim
COPY app.py .
# Add health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["python", "app.py"]
# Use exec form to ensure proper signal handling
CMD ["python", "app.py"] # โ
Receives SIGTERM
# Not shell form
CMD python app.py # โ Shell doesn't forward signals
LABEL org.opencontainers.image.title="MyApp"
LABEL org.opencontainers.image.version="1.2.3"
LABEL org.opencontainers.image.authors="team@example.com"
LABEL org.opencontainers.image.source="https://github.com/org/repo"
FROM python:3.11-slim AS builder
# Prevent Python from writing pyc files
ENV PYTHONDONTWRITEBYTECODE=1
# Prevent Python from buffering stdout/stderr
ENV PYTHONUNBUFFERED=1
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
# Install to user site-packages
RUN pip install --user --no-cache-dir -r requirements.txt
# Runtime stage
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH=/root/.local/bin:$PATH
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0"]
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
# Install production dependencies only
RUN npm ci --only=production && \
# Remove npm cache
npm cache clean --force
# Runtime stage
FROM node:20-alpine
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodeuser -u 1001
WORKDIR /app
COPY --from=builder --chown=nodeuser:nodejs /app/node_modules ./node_modules
COPY --chown=nodeuser:nodejs . .
USER nodeuser
EXPOSE 3000
CMD ["node", "server.js"]
# Builder stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# Runtime stage - minimal scratch image
FROM scratch
# Copy CA certificates for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy binary
COPY --from=builder /app/main /main
EXPOSE 8080
ENTRYPOINT ["/main"]
Result: 1GB โ 15MB (98.5% reduction!)
โ Bad (installs hundreds of unnecessary packages):
RUN apt-get install curl
โ Good (minimal installation):
RUN apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
โ Bad (ADD has implicit behavior):
ADD requirements.txt . # Can extract tarballs, fetch URLs
โ Good (COPY is explicit):
COPY requirements.txt . # Only copies files
โ Bad (can't reference previous stages):
FROM python:3.11
RUN pip install -r requirements.txt
FROM python:3.11-slim
# Can't copy from previous stage!
โ Good (named stages):
FROM python:3.11 AS builder
RUN pip install -r requirements.txt
FROM python:3.11-slim
COPY --from=builder /root/.local /root/.local
Without .dockerignore:
โ Bad (no control over patch versions):
FROM python:3.11
โ Good (pin exact version):
FROM python:3.11.9-slim-bookworm
Before (1.2GB image, 5min build):
FROM python:3.11
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["uvicorn", "app:app"]
After (140MB image, 2min build):
FROM python:3.11-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends gcc && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM python:3.11-slim
ENV PATH=/root/.local/bin:$PATH
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY app.py .
COPY src/ ./src/
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
CMD ["uvicorn", "app:app", "--host", "0.0.0.0"]
Results:
Image Size:
apt-get install && rm -rf)--no-install-recommends with apt-getpip --no-cache-dir, npm cache clean)Build Speed:
Security:
Production:
Official Docker Documentation:
Security Scanning:
Base Images: