Skip to content
BotServBotServ
DockerImageOptimizationMulti-StageLayer

Optimize Docker Images

Reduce Docker image size and speed. Layer optimization, multi-stage builds, .dockerignore and best practices.

S

schutzgeist

3 min read
Optimize Docker Images

Optimizing Docker Images

What this article covers

  • Why smaller images matter.
  • How layers and caching work.
  • Multi-stage builds and .dockerignore.
  • Base image selection.
  • Security and performance best practices.

Introduction: Optimizing Docker Images

Large Docker images slow down builds, deployments, and downloads. They expand your attack surface by including unnecessary software and waste storage capacity. With targeted optimization, you can often shrink images from several gigabytes to a few hundred megabytes without sacrificing functionality.

This article shows how to make Docker images small, fast, and secure.

Key Concepts

  • Layer: A distinct level within a Docker image.
  • Caching: Reusing unchanged layers during subsequent builds.
  • Multi-stage build: Multiple build phases in a single Dockerfile.
  • .dockerignore: File that excludes unwanted files from the build context.
  • Base image: The foundation layer for your image.
  • Distroless: Minimal image without a shell or unnecessary tools.
  • Squashing: Combining multiple layers into one.

Why Optimize Images?

  • Faster builds and deployments.
  • Lower storage requirements.
  • Reduced attack surface.
  • Less bandwidth needed for distribution.
  • Easier maintenance and auditing.

Choosing a Base Image

Large vs. Small

BaseSizeAdvantageDrawback
ubuntu~78 MBPlenty of toolsRelatively large
debian:slim~30 MBWell balancedFewer tools
alpine~5 MBVery compactmusl compatibility issues
distrolessunder 5 MBMinimal footprintNo shell, harder debugging

Recommendations

  • For Python: python:3.11-slim.
  • For Node.js: node:20-alpine or slim.
  • For Go: golang:alpine or scratch for compiled binaries.
  • For critical environments: distroless.

Multi-Stage Builds

# Builder
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Runtime
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]

Only runtime files end up in the final image.

Optimizing Layers and Caching

Each instruction in a Dockerfile creates a layer. Place frequently changing commands at the end:

# Dependencies first, rarely change
COPY package*.json ./
RUN npm ci

# Source code last, changes often
COPY . .

This makes effective use of layer caching.

.dockerignore

Prevents unnecessary files from entering the build context:

node_modules
.git
*.log
.env
Dockerfile
.dockerignore
README.md
tests/

Combine Commands

Instead of:

RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

use:

RUN apt-get update && apt-get install -y curl && apt-get clean && rm -rf /var/lib/apt/lists/*

This creates a single layer and removes caches.

Remove Unneeded Tools

  • Keep build tools like compilers only in builder stages.
  • Run apt-get clean and remove package lists.
  • Use npm ci instead of npm install.
  • Exclude documentation and examples from the image.
  • Don’t include test directories in production images.

Non-Root User

FROM node:20-slim
RUN useradd -m appuser
USER appuser

This reduces risk if a container is compromised.

Labels and Metadata

LABEL maintainer="team@example.com"
LABEL org.opencontainers.image.source="https://github.com/user/repo"
LABEL org.opencontainers.image.version="1.0.0"

Add Health Checks

HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:3000/health || exit 1

Analyzing Images

Tools for inspection:

docker history my-image:latest
dive my-image:latest

dive reveals which layers consume the most space.

Example: Optimized Dockerfile

# Builder
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

# Runtime
FROM python:3.11-slim
WORKDIR /app
RUN useradd -m appuser
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser . .
USER appuser
ENV PATH=/home/appuser/.local/bin:$PATH
HEALTHCHECK CMD curl -f http://localhost:8000/health || exit 1
CMD ["python", "main.py"]

Security

  • Use fresh base images regularly.
  • Scan images for vulnerabilities.
  • Never embed secrets in images.
  • Install only necessary packages.
  • Consider a read-only filesystem.
  • Avoid latest tags.

Common Pitfalls

  • COPY . . too early: Invalidates cache on every code change.
  • No .dockerignore: Build context becomes massive.
  • Build tools in final image: Results in larger, less secure images.
  • Running as root: Increases risk.
  • Too many layers: Each RUN creates one.
  • Oversized base images: Unnecessary when slim variants exist.
  • Skipping apt clean: Layers stay larger than needed.

Further Reading and Resources

FAQ: Optimizing Docker Images

How small should an image be? As small as possible without losing functionality. Often under 100 MB for simple applications.

Is Alpine always better? Not necessarily. musl compatibility can cause issues with certain libraries.

Do I need multi-stage builds? Usually yes for compiled languages and Node.js.

What is distroless? An extremely minimal image without a shell, containing only runtime files.

How do I check image size? Use docker images, docker history, or dive.

Sources and Further Reading

Summary: Optimizing Docker Images

Optimized Docker images are smaller, faster, and more secure. The key measures are selecting a lean base image, using multi-stage builds, leveraging layer caching, writing a good .dockerignore, and removing unnecessary tools. Non-root users, health checks, and labels complete the picture. Regular vulnerability scans and analysis with dive keep you in control of size and security.

Back to Blog
Share:

Related Posts