Docker Image Optimization: Best Practices for Smaller, Faster Images
TLDR
Optimize Docker images by using multi-stage builds, choosing minimal base images (Alpine, Distroless), leveraging layer caching, minimizing layers, removing build dependencies, and using .dockerignore. These practices can reduce image size by 70-90% and significantly improve build and deployment times.
Why Docker Image Optimization Matters
Docker image size directly impacts:
- Build times - Smaller images build faster
- Storage costs - Less disk space in registries and hosts
- Deployment speed - Faster image pulls across environments
- Security - Fewer packages = smaller attack surface
- Network bandwidth - Reduced data transfer costs
A typical unoptimized Node.js app can be 1GB+, while an optimized version might be just 50-100MB.
1. Use Multi-Stage Builds
Multi-stage builds let you use one image for building and another for runtime, keeping only what you need in the final image.
Before (Single Stage):
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]
After (Multi-Stage):
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # Install all dependencies including devDependencies for build
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
# Install only production dependencies
COPY package*.json ./
RUN npm ci --only=production
# Copy build artifacts
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/index.js"]
Result: Image size reduced from 1.2GB to 180MB (85% smaller).
2. Choose the Right Base Image
Base image selection has the biggest impact on final image size.
Base Image Comparison
| Base Image | Size | Use Case |
|---|---|---|
node:20 |
1.1GB | Development only |
node:20-slim |
240MB | General production |
node:20-alpine |
140MB | Minimal production |
gcr.io/distroless/nodejs20 |
120MB | Maximum security |
scratch |
0MB | Static binaries only |
Alpine Linux:
FROM node:20-alpine
# Install only necessary packages
RUN apk add --no-cache dumb-init
Distroless (Google):
FROM gcr.io/distroless/nodejs20-debian12
COPY --chown=nonroot:nonroot /app /app
WORKDIR /app
USER nonroot
CMD ["dist/index.js"]
Scratch (for Go/Rust):
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o app
FROM scratch
COPY --from=builder /app/app /app
CMD ["/app"]
3. Optimize Layer Caching
Docker caches each layer. Order instructions from least to most frequently changing.
Poor Caching (rebuilds everything on code change):
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "start"]
Optimized Caching:
FROM node:20-alpine
WORKDIR /app
# Cache dependencies separately
COPY package*.json ./
RUN npm ci --only=production
# Copy source code last
COPY . .
CMD ["node", "index.js"]
Now changing source code doesn't invalidate the dependency layer.
4. Minimize Layers and Clean Up
Each RUN, COPY, and ADD creates a layer. Combine commands and clean up in the same layer.
Before (Multiple Layers):
RUN apt-get update
RUN apt-get install -y curl
RUN curl -o file.tar.gz https://example.com/file.tar.gz
RUN tar -xzf file.tar.gz
RUN rm file.tar.gz
After (Single Layer with Cleanup):
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
curl -o file.tar.gz https://example.com/file.tar.gz && \
tar -xzf file.tar.gz && \
rm file.tar.gz && \
apt-get remove -y curl && \
apt-get autoremove -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
5. Use .dockerignore
Prevent unnecessary files from being copied into the image.
.dockerignore:
# Version control
.git
.gitignore
# Dependencies
node_modules
npm-debug.log*
# IDE
.vscode
.idea
*.swp
# Testing
coverage
.nyc_output
*.test.js
# Documentation
README.md
docs/
# Environment
.env
.env.local
# Build artifacts
dist/
build/
*.log
# OS files
.DS_Store
Thumbs.db
This can prevent hundreds of megabytes from being copied unnecessarily.
6. Remove Build Dependencies
Install build tools, compile, then remove them in the same layer.
Python Example:
FROM python:3.11-slim
WORKDIR /app
# Install dependencies with build tools, then clean up
COPY requirements.txt .
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc && \
pip install --no-cache-dir -r requirements.txt && \
apt-get purge -y --auto-remove gcc && \
rm -rf /var/lib/apt/lists/*
COPY . .
CMD ["python", "app.py"]
7. Optimize Package Manager Usage
APT (Debian/Ubuntu)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
package1 \
package2 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
APK (Alpine)
RUN apk add --no-cache package1 package2
NPM/Yarn
# Use ci for reproducible builds
RUN npm ci --only=production
# Or with Yarn
RUN yarn install --frozen-lockfile --production && \
yarn cache clean
Pip
RUN pip install --no-cache-dir -r requirements.txt
8. Use Specific Version Tags
Always pin base image versions for reproducibility.
Bad:
FROM node
FROM node:latest
Good:
FROM node:20.11.0-alpine3.19
9. Security Best Practices
Run as Non-Root User
FROM node:20-alpine
# Create app user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
COPY --chown=nodejs:nodejs . .
USER nodejs
CMD ["node", "index.js"]
Scan for Vulnerabilities
# Using Docker Scout
docker scout cves myimage:latest
# Using Trivy
trivy image myimage:latest
# Using Snyk
snyk container test myimage:latest
10. Real-World Example: Complete Optimization
Before (1.2GB):
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
After (45MB - 96% smaller):
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci && \
npm cache clean --force
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# Install only production dependencies
COPY --chown=nodejs:nodejs package*.json ./
RUN npm ci --only=production && npm cache clean --force
# Copy build artifacts
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
USER nodejs
EXPOSE 3000
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]
11. Build Optimization Tips
Use BuildKit
Enable Docker BuildKit for faster builds with better caching:
export DOCKER_BUILDKIT=1
docker build -t myapp .
Parallel Builds
BuildKit supports concurrent stage execution:
# syntax=docker/dockerfile:1
# Define base image first
FROM node:20-alpine AS base
WORKDIR /app
# Dependencies stage
FROM base AS deps
COPY package*.json ./
RUN npm ci
# Build stage
FROM base AS build
COPY package*.json ./
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Test stage (runs in parallel with build)
FROM base AS test
COPY package*.json ./
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm test
Cache Mounts
BuildKit cache mounts persist cache across builds:
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production
12. Measuring Success
Check Image Size
docker images myapp
docker history myapp:latest
Dive Tool (Layer Analysis)
# Install dive
wget https://github.com/wagoodman/dive/releases/download/v0.11.0/dive_0.11.0_linux_amd64.deb
sudo dpkg -i dive_0.11.0_linux_amd64.deb
# Analyze image
dive myapp:latest
Docker Slim
Automatically minify images:
docker-slim build --http-probe myapp:latest
Common Pitfalls to Avoid
- Installing unnecessary packages - Use
--no-install-recommendswith apt - Not cleaning package manager caches - Always clean in the same RUN command
- Copying entire context - Use
.dockerignoreextensively - Using
latesttags - Pin specific versions - Running as root - Always create and use a non-root user
- Ignoring layer order - Put frequently changing files last
- Not using multi-stage builds - Always separate build and runtime stages
Optimization Checklist
- Use multi-stage builds
- Choose minimal base image (Alpine/Distroless)
- Create and use
.dockerignorefile - Order layers by change frequency
- Combine RUN commands and clean up in same layer
- Use
--no-cacheand--no-install-recommends - Remove build dependencies after compilation
- Pin specific image versions
- Run as non-root user
- Enable BuildKit for builds
- Scan images for vulnerabilities
- Measure and track image sizes
Conclusion
Docker image optimization is not optional—it's essential for production deployments. By following these best practices, you can reduce image sizes by 70-96%, improve build times, reduce costs, and enhance security. Start with multi-stage builds and minimal base images, then progressively apply other optimizations.
Remember: every megabyte saved is multiplied across your entire infrastructure—CI/CD pipelines, registries, and production deployments.
Related Resources
- Advanced Docker Features — BuildKit, health checks, and more
- Docker Security Best Practices — secure your optimized images
- COPY vs ADD in Dockerfiles — choose the right instruction
- Docker Multi-Stage Build Exercise — hands-on optimization
- Docker Security Checklist — verify your setup
- Introduction to Docker: Best Practices — comprehensive guide
- DevOps Survival Guide — broader DevOps learning
We earn commissions when you shop through the links below.
DigitalOcean
Cloud infrastructure for developers
Simple, reliable cloud computing designed for developers
DevDojo
Developer community & tools
Join a community of developers sharing knowledge and tools
SMTPfast
Developer-first email API
Send transactional and marketing email through a clean REST API. Detailed logs, webhooks, and embeddable signup forms in one dashboard.
QuizAPI
Developer-first quiz platform
Build, generate, and embed quizzes with a powerful REST API. AI-powered question generation and live multiplayer.
Want to support DevOps Daily and reach thousands of developers?
Become a SponsorFound an issue?
Related Posts
Also worth your time on this topic
Docker Multi-Stage Build Optimization
Learn to create efficient Docker images using multi-stage builds to reduce image size and improve security.
60 minutes
5 Advanced Docker Features Worth Knowing
Go beyond Docker basics with BuildKit, multi-stage builds, health checks, init processes, and build secrets. Learn practical techniques that improve security, performance, and reliability.
Docker Security Hardening Checklist
Comprehensive security checklist for hardening Docker containers, images, and runtime environments.
60-90 minutes