Docker Image Optimization: Best Practices

Docker Image Optimization: Best Practices

Docker Image Optimization: Best Practices

Learn how to create smaller, faster, and more secure Docker images

Introduction

Docker has become an essential tool in modern software development, but poorly optimized images can lead to slow builds, large storage requirements, and security vulnerabilities. In this post, we'll explore practical techniques to optimize your Docker images for size, performance, and security.

1. Use Appropriate Base Images

The Problem:

Many developers default to using heavy base images like ubuntu or node:latest which include unnecessary packages and take up significant space.

The Solution:

Choose minimal base images that contain only what your application needs:

  • Use Alpine Linux variants when possible (node:alpine, python:alpine)
  • Consider distroless images for production (gcr.io/distroless/nodejs)
  • Use specific version tags instead of latest
# Instead of this:
FROM ubuntu:latest

# Use this:
FROM alpine:3.16

# Or for Node.js applications:
FROM node:16-alpine

2. Leverage Multi-Stage Builds

The Problem:

Development dependencies, build tools, and intermediate files end up in your final image, increasing its size unnecessarily.

The Solution:

Use multi-stage builds to separate build dependencies from runtime dependencies:

# Build stage
FROM node:16 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]

3. Optimize Dockerfile Instructions

The Problem:

Inefficient Dockerfile instructions lead to larger images and slower builds.

The Solution:

  • Combine RUN commands to reduce layers
  • Order instructions from least to most frequently changing
  • Clean up unnecessary files in the same layer they were created
# Instead of this:
RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2
RUN rm -rf /var/lib/apt/lists/*

# Use this:
RUN apt-get update && \
    apt-get install -y package1 package2 && \
    rm -rf /var/lib/apt/lists/*

4. Minimize Layer Count

The Problem:

Each instruction in a Dockerfile creates a new layer, increasing image size and build time.

The Solution:

  • Combine related commands into single RUN instructions
  • Use && to chain commands
  • Clean up temporary files in the same layer
# Instead of multiple layers:
RUN curl -sSL https://example.com/tool.tar.gz -o tool.tar.gz
RUN tar -xzf tool.tar.gz -C /usr/local/bin
RUN rm tool.tar.gz

# Use a single layer:
RUN curl -sSL https://example.com/tool.tar.gz -o tool.tar.gz && \
    tar -xzf tool.tar.gz -C /usr/local/bin && \
    rm tool.tar.gz

5. Use .dockerignore

The Problem:

Unnecessary files (node_modules, logs, documentation) are copied into the image, increasing its size.

The Solution:

Create a .dockerignore file to exclude unnecessary files from being copied into the image:

# .dockerignore
.git
.gitignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
README.md
.env
.nyc_output
coverage
*.log
*.tmp
*.tar.gz

6. Reduce Dependencies

The Problem:

Applications often include development dependencies or unnecessary packages in production images.

The Solution:

  • Only install production dependencies (npm ci --only=production)
  • Remove package manager caches in the same layer
  • Consider using smaller alternatives to common packages
# For Node.js applications
RUN npm ci --only=production && \
    npm cache clean --force

# For Python applications
RUN pip install --no-cache-dir -r requirements.txt

# For APT packages
RUN apt-get update && \
    apt-get install -y --no-install-recommends package-name && \
    rm -rf /var/lib/apt/lists/*

7. Security Considerations

The Problem:

Running containers as root, including sensitive information, or using outdated packages creates security vulnerabilities.

The Solution:

  • Run containers as non-root users
  • Regularly update base images and dependencies
  • Use secrets management for sensitive information
  • Scan images for vulnerabilities
# Create a non-root user
RUN addgroup -g 1000 appuser && \
    adduser -u 1000 -G appuser -D appuser

# Use the non-root user
USER appuser

# Instead of copying secrets, use Docker secrets or environment variables
# Don't do this:
COPY .env .

# Instead, use:
# docker run -e ENV_VAR=value my-image
# or Docker secrets for sensitive data

8. Use Specific Tags

The Problem:

Using the latest tag can lead to unpredictable behavior and makes it difficult to track which version is running.

The Solution:

Always use specific version tags for base images and your own images:

# Instead of:
FROM node:latest

# Use:
FROM node:16.18.1-alpine

# Tag your own images with specific versions
docker build -t my-app:1.2.3 .
docker build -t my-app:latest .  # Optional, but not recommended for production

9. Regularly Update and Scan Images

The Problem:

Outdated images contain known vulnerabilities that can be exploited.

The Solution:

  • Regularly update base images and dependencies
  • Use vulnerability scanning tools
  • Rebuild images when dependencies update
# Use tools like Docker Scout, Trivy, or Grype to scan images
docker scout quickview my-image:latest

# Set up automated builds to rebuild when base images update

10. Measure and Compare

The Problem:

Without measuring image size and build time, it's difficult to know if optimizations are working.

The Solution:

  • Compare image sizes before and after optimizations
  • Measure build times
  • Use tools to analyze image layers
# Check image size
docker images

# Analyze image layers
docker history my-image

# Use dive for detailed layer analysis
dive my-image

Conclusion

Optimizing Docker images is an ongoing process that pays dividends in faster deployments, reduced storage costs, and improved security. By following these best practices, you can create images that are efficient, secure, and maintainable.

Remember to:

  • Start with minimal base images
  • Use multi-stage builds
  • Reduce layers and clean up in the same layer
  • Use .dockerignore to exclude unnecessary files
  • Regularly update and scan your images

© 2025 DevOps Blog - Docker Optimization Guide

Comments

Popular posts from this blog

Real-world Terraform scenarios to test and improve your Infrastructure as Code skills

Azure Kubernetes Service (AKS) Complete Guide

Automate Your DevOps Documentation: `iac-to-docs` Lands on PyPI with AI Power