Optimizing Docker Build Performance and Image Size
Introduction
Efficient Docker builds improve development velocity and reduce deployment costs. This guide covers multi-stage builds, layer caching, and image optimization techniques.
Prerequisites
- Docker >=20.10
- Basic Dockerfile knowledge
Step 1: Multi-Stage Build Example
Create optimized Dockerfile
:
# Multi-stage build for Node.js application
FROM node:18-alpine AS base
WORKDIR /app
COPY package*.json ./
# Dependencies stage
FROM base AS deps
RUN npm ci --only=production --frozen-lockfile
# Build stage
FROM base AS build
RUN npm ci --frozen-lockfile
COPY . .
RUN npm run build
# Runtime stage
FROM node:18-alpine AS runtime
WORKDIR /app
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy only necessary files
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/public ./public
# Set ownership
RUN chown -R nextjs:nodejs /app
USER nextjs
EXPOSE 3000
CMD ["node", "dist/server.js"]
Step 2: Optimize .dockerignore
Create comprehensive .dockerignore
:
# Version control
.git
.gitignore
# Dependencies
node_modules
npm-debug.log*
# Build outputs
dist
build
.next
# Development files
.env.local
.env.development
docker-compose.yml
docker-compose.override.yml
# Documentation
README.md
docs/
# IDE files
.vscode/
.idea/
*.swp
*.swo
# OS generated files
.DS_Store
Thumbs.db
# Test files
coverage/
.nyc_output
test/
*.test.js
*.spec.js
# Logs
*.log
logs/
Step 3: Layer Caching Strategy
Optimize layer ordering for maximum cache reuse:
FROM node:18-alpine AS base
# Install system dependencies (rarely changes)
RUN apk add --no-cache \
dumb-init \
&& rm -rf /var/cache/apk/*
WORKDIR /app
# Copy package files first (changes less frequently)
COPY package*.json ./
COPY yarn.lock ./
# Install dependencies (cached if package.json unchanged)
RUN npm ci --only=production --frozen-lockfile
# Copy source code last (changes most frequently)
COPY . .
# Build application
RUN npm run build
# Runtime optimizations
ENV NODE_ENV=production
USER node
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]
Step 4: BuildKit Features
Enable BuildKit for advanced features:
export DOCKER_BUILDKIT=1
Use cache mounts and bind mounts:
# syntax=docker/dockerfile:1
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
# Use cache mount for npm cache
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production --frozen-lockfile
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
# Cache mount for build dependencies
RUN --mount=type=cache,target=/root/.npm \
npm ci --frozen-lockfile
COPY . .
RUN npm run build
FROM node:18-alpine AS runtime
WORKDIR /app
# Copy from previous stages
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]
Step 5: Distroless Images
Use minimal base images:
# Build stage
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --frozen-lockfile
COPY . .
RUN npm run build
# Production stage with distroless
FROM gcr.io/distroless/nodejs18-debian11
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./
EXPOSE 3000
CMD ["dist/server.js"]
Step 6: Security Optimization
Secure Dockerfile practices:
FROM node:18-alpine AS base
# Security updates
RUN apk update && apk upgrade && \
apk add --no-cache dumb-init && \
rm -rf /var/cache/apk/*
# Create app directory and user
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
FROM base AS deps
COPY package*.json ./
USER nextjs
# Install with security audit
RUN npm ci --only=production --frozen-lockfile && \
npm audit --audit-level moderate
FROM base AS build
COPY package*.json ./
RUN npm ci --frozen-lockfile
COPY --chown=nextjs:nodejs . .
USER nextjs
RUN npm run build
FROM base AS runtime
COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=build --chown=nextjs:nodejs /app/dist ./dist
# Security: Remove package managers and add healthcheck
RUN npm uninstall -g npm && \
rm -rf /usr/local/lib/node_modules/npm
USER nextjs
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
EXPOSE 3000
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]
Step 7: Build Performance Tools
Use build-time optimization tools:
#!/bin/bash
set -e
# Enable experimental features
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
# Build with cache from registry
docker buildx build \
--platform linux/amd64,linux/arm64 \
--cache-from type=registry,ref=myapp:cache \
--cache-to type=registry,ref=myapp:cache,mode=max \
--push \
-t myapp:latest .
# Analyze image size
docker images myapp:latest --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
# Security scan
docker scout cves myapp:latest
Step 8: Image Analysis and Monitoring
Create image analysis script:
#!/bin/bash
IMAGE_NAME=${1:-myapp:latest}
echo "=== Image Analysis for $IMAGE_NAME ==="
# Basic image info
echo "Image size:"
docker images $IMAGE_NAME --format "table {{.Size}}"
# Layer analysis
echo -e "\nLayer sizes:"
docker history $IMAGE_NAME --format "table {{.CreatedBy}}\t{{.Size}}" | head -10
# Security vulnerabilities
echo -e "\nSecurity scan:"
docker scout cves $IMAGE_NAME --only-severity critical,high
# Resource usage test
echo -e "\nStarting container for resource test..."
CONTAINER_ID=$(docker run -d --memory=256m --cpus=0.5 $IMAGE_NAME)
sleep 5
echo "Container stats:"
docker stats $CONTAINER_ID --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}"
# Cleanup
docker stop $CONTAINER_ID
docker rm $CONTAINER_ID
Step 9: CI/CD Integration
GitHub Actions workflow for optimized builds:
name: Optimized Docker Build
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Image Analysis
run: |
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest ghcr.io/${{ github.repository }}:latest \
--ci
Summary
Docker build optimization involves multi-stage builds, layer caching, minimal base images, and security practices. Use BuildKit features, analyze image sizes, and implement CI/CD pipelines for consistent, efficient builds.