2025-12-27
12 min read

Docker Image Optimization: Best Practices for Smaller, Faster Images

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

  1. Installing unnecessary packages - Use --no-install-recommends with apt
  2. Not cleaning package manager caches - Always clean in the same RUN command
  3. Copying entire context - Use .dockerignore extensively
  4. Using latest tags - Pin specific versions
  5. Running as root - Always create and use a non-root user
  6. Ignoring layer order - Put frequently changing files last
  7. 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 .dockerignore file
  • Order layers by change frequency
  • Combine RUN commands and clean up in same layer
  • Use --no-cache and --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.

Proudly Sponsored By

These amazing companies help us create free, high-quality DevOps content for the community

Want to support DevOps Daily and reach thousands of developers?

Become a Sponsor
Published: 2025-12-27|Last updated: 2025-12-27T09:00:00Z

Found an issue?