Dockerfile Optimization — Smaller, Faster Images
Advertisement
Dockerfile Optimization — Smaller, Faster Images
Optimized Dockerfiles build faster, deploy quicker, and consume less storage. Learn advanced techniques to create production-grade images efficiently.
Introduction
Large Docker images lead to slow deployments, higher bandwidth costs, and larger attack surfaces. Optimizing your Dockerfile is essential for scalable DevOps practices.
- Dockerfile Optimization — Smaller, Faster Images
- Understanding Image Layers
- Layer Caching Strategy
- Inefficient Approach
- Optimized Approach
- Base Image Selection
- Size Comparison
- Using Alpine Linux
- Multi-Stage Builds
- Build Context Reduces Size
- Multi-Stage Approach: 350 MB
- Advanced Multi-Stage Example
- Reducing Dependencies
- Frontend Application
- Using distroless Images
- Combining Techniques
- Complete Optimization Example
- Build Optimization Tips
- Parallel Builds
- BuildKit Features
- .dockerignore
- Benchmarking Optimization
- Performance Gains
- FAQ
Understanding Image Layers
Each Dockerfile instruction creates a layer. Docker caches layers to speed up builds.
FROM node:18 # Layer 1: Base image
WORKDIR /app # Layer 2: Working directory
COPY package*.json ./ # Layer 3: Copy files (frequently invalidates cache)
RUN npm ci # Layer 4: Install dependencies
COPY . . # Layer 5: Copy app code (frequently invalidates cache)
RUN npm run build # Layer 6: Build step
CMD ["node", "server.js"] # Layer 7: Default command
Layer Caching Strategy
Order instructions from least to most frequently changed:
Inefficient Approach
FROM node:18
WORKDIR /app
# Problem: Large COPY invalidates all following layers
COPY . .
# These run unnecessarily on every build
RUN npm ci
RUN npm run build
CMD ["node", "server.js"]
Optimized Approach
FROM node:18
WORKDIR /app
# Stable layer: only changes when dependencies change
COPY package*.json ./
RUN npm ci --only=production
# Variable layers: change with source code
COPY . .
RUN npm run build
CMD ["node", "server.js"]
Build times after first build:
- Inefficient: 2+ minutes
- Optimized: 5-10 seconds (using cache)
Base Image Selection
Size Comparison
ubuntu:22.04 → 77 MB
debian:bookworm → 124 MB
node:18 → 1000 MB
node:18-alpine → 173 MB
python:3.11 → 967 MB
python:3.11-alpine → 129 MB
golang:1.21 → 800 MB
golang:1.21-alpine → 372 MB
Using Alpine Linux
# Before: 1000 MB
FROM node:18
RUN apt-get update && apt-get install -y curl
COPY . .
RUN npm ci --only=production
CMD ["node", "server.js"]
# After: 173 MB (82% reduction)
FROM node:18-alpine
RUN apk add --no-cache curl
COPY . .
RUN npm ci --only=production
CMD ["node", "server.js"]
Key differences with Alpine:
- Uses
apkinstead ofapt - Use
--no-cacheto prevent package caching - Some packages have different names
Multi-Stage Builds
Multi-stage builds separate build and runtime stages, keeping only necessary artifacts:
Build Context Reduces Size
# Traditional approach: 1.2 GB
FROM node:18
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
CMD ["node", "dist/server.js"]
Multi-Stage Approach: 350 MB
# Stage 1: Builder
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Runtime (lightweight)
FROM node:18-alpine
WORKDIR /app
# Copy only built artifacts from builder
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]
Advanced Multi-Stage Example
# Stage 1: Dependencies
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: Builder
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Runtime (production)
FROM node:18-alpine AS runtime
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY /app/dist ./dist
COPY /app/package.json ./
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
HEALTHCHECK CMD node healthcheck.js
CMD ["node", "dist/server.js"]
Reducing Dependencies
Frontend Application
# Inefficient: 1.5 GB (includes build tools in final image)
FROM node:18
WORKDIR /app
COPY . .
RUN npm ci && npm run build
CMD ["npm", "start"]
# Optimized: 80 MB (build tools removed)
FROM node:18-alpine AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build
FROM nginx:alpine
COPY /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Using distroless Images
# For Go applications: 50 MB
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o app
FROM gcr.io/distroless/base
COPY /app/app /app
CMD ["/app"]
Combining Techniques
Complete Optimization Example
# Build stage
FROM node:18-alpine AS builder
WORKDIR /build
# Copy package files only
COPY package*.json ./
# Install all dependencies (including dev)
RUN npm ci
# Copy source code
COPY . .
# Build application
RUN npm run build
# Prune dev dependencies
RUN npm prune --only=production
# Runtime stage
FROM node:18-alpine
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Create non-root user
RUN addgroup -g 1001 app && \
adduser -D -u 1001 -G app app
WORKDIR /app
# Copy only necessary files
COPY /build/dist ./dist
COPY /build/node_modules ./node_modules
COPY /build/package.json ./
USER app
EXPOSE 3000
HEALTHCHECK \
CMD node healthcheck.js || exit 1
ENTRYPOINT ["/usr/sbin/dumb-init", "--"]
CMD ["node", "dist/server.js"]
Build Optimization Tips
Parallel Builds
# Buildkit enables parallel stages
DOCKER_BUILDKIT=1 docker build -t myapp:1.0 .
BuildKit Features
# Syntax directive (requires BuildKit)
# syntax=docker/dockerfile:1.4
FROM node:18-alpine AS builder
WORKDIR /app
# Mount secrets during build (not in final image)
RUN \
echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" > .npmrc && \
npm ci
COPY . .
RUN npm run build
Build with secrets:
DOCKER_BUILDKIT=1 docker build \
--secret npm_token=/path/to/token \
-t myapp:1.0 .
.dockerignore
# Version control
.git
.gitignore
# Dependencies
node_modules
package-lock.json
# Build artifacts
dist
build
.next
# Testing
coverage
test
# Environment
.env
.env.local
# IDE
.vscode
.idea
.DS_Store
# OS
Thumbs.db
Benchmarking Optimization
# Build and check image size
docker build -t myapp:v1 .
docker images myapp:v1
# Compare with previous version
docker images | grep myapp
# Inspect layers
docker history myapp:v1
Example output:
IMAGE CREATED CREATED BY SIZE
a1b2c3d4 2 minutes ago /bin/sh -c dumb-init node dist/server.js 5MB
b2c3d4e5 2 minutes ago WORKDIR /app 0B
c3d4e5f6 3 minutes ago COPY --from=builder /build/package.json ./ 4MB
Performance Gains
Typical optimization results:
| Aspect | Before | After | Improvement |
|---|---|---|---|
| Image Size | 1.2 GB | 200 MB | 83% smaller |
| Build Time | 120s | 8s | 93% faster |
| Push Time | 2 min | 10s | 92% faster |
| Pull Time | 1.5 min | 8s | 94% faster |
FAQ
Q: Should I always use Alpine Linux? A: Alpine is great for most cases but has limitations. Some packages have different names or behavior. Test thoroughly with Alpine before production.
Q: What's the downside of multi-stage builds? A: Slightly more complex Dockerfile syntax and build takes longer. Performance benefits outweigh these minor downsides.
Q: How do I know if my image is optimized? A: Use docker history to identify large layers. If base image plus dependencies is <500MB for most apps, you're on track.
Advertisement