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.
These amazing companies help us create free, high-quality DevOps content for the community
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
Want to support DevOps Daily and reach thousands of developers?
Become a SponsorFound an issue?