Skip to content
Home
Docker Image Optimization: 90% Size Reduction Guide

Docker Image Optimization: 90% Size Reduction Guide

Docker Docker 8 min read 1645 words Beginner ExcellentWiki Editorial Team

A well-optimized Docker image is smaller, faster to build, more secure, and cheaper to store. Following established Dockerfile best practices can reduce image size by 90% or more — shrinking a 1.2 GB Python image to under 100 MB — while simultaneously improving build times through effective layer caching. These patterns come from production deployments at companies like Google, Netflix, and Shopify, where image optimization directly impacts infrastructure costs and deployment velocity.

Why Image Size Matters

Image size affects every part of the container lifecycle. Build time increases with every unnecessary layer. Push and pull times on container registries scale linearly with size — a 100 MB image pushes in seconds, while a 1 GB image takes minutes. Storage costs in registries compound across multiple image versions. Startup time on Kubernetes nodes increases because the kubelet must pull the image before starting the pod. In auto-scaling scenarios, slower image pulls delay scale-up and can cause capacity shortfalls during traffic spikes.

The Cloud Native Computing Foundation (CNCF) recommends keeping images under 500 MB for optimal Kubernetes performance. Images under 100 MB achieve sub-second pull times on nodes with cached base layers. According to Docker’s performance documentation, every 100 MB of image size adds approximately 10-15 seconds to pull time on a typical 100 Mbps connection, making optimization directly proportional to deployment speed.

Base Image Selection

Always pin base images to a specific version or digest. Avoid :latest or omitting the tag entirely.

# Bad — unpredictable builds
FROM python

# Good — specific version
FROM python:3.11-slim

# Best — pin to digest for reproducibility
FROM python:3.11-slim@sha256:abc123def456...

Pinning to a digest guarantees every build uses exactly the same base image, even if the tag is updated upstream. This eliminates a class of build failures where a base image update introduces breaking changes. Docker’s security team recommends digest pinning for production builds, and it is enforced by Docker’s content trust mechanism.

Base Image Size Comparison

ImageSizeUse Case
python:latest~900 MBFull OS with build tools
python:3.11-slim~120 MBDebian slim — runtime only
python:3.11-alpine~50 MBAlpine Linux — minimal
gcr.io/distroless/python3~45 MBDistroless — no shell, no package manager
scratch0 BEmpty — statically linked binaries

The choice between Alpine and Slim depends on compatibility requirements. Slim (Debian-based) uses glibc, which most Linux software targets. Alpine uses musl libc, which can cause compatibility issues with Python wheels, Java JNI libraries, and some C extensions. KodeKloud’s Docker Deep Dive course recommends using Alpine only when compatibility is confirmed and defaulting to Slim for general-purpose images.

Multi-Stage Builds

Multi-stage builds use multiple FROM statements, each beginning a new stage. Only the final stage becomes the published image.

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

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

The final image is ~25 MB instead of ~300 MB. Build tools, node_modules, and source maps stay in the builder stage and are discarded. This technique alone typically reduces image size by 80-95%. Docker’s official documentation highlights multi-stage builds as “the single most effective optimization” for most containerized applications.

Language-Specific Multi-Stage Patterns

For Go applications, use scratch or distroless base images because Go binaries are statically linked:

FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .

FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]

Final image size: ~15 MB — just the compiled Go binary with no operating system overhead. Google’s distroless images provide a middle ground between Alpine and scratch, containing only the application runtime dependencies without a shell or package manager.

Layer Caching Optimization

Docker builds images layer by layer. Each instruction creates a layer that Docker caches. If a layer has not changed (the instruction and its context are the same as in a previous build), Docker reuses the cached layer.

Order Instructions for Maximum Cache Hits

Order instructions from least to most frequently changing:

# 1. Install system dependencies (rarely changes)
RUN apt-get update && apt-get install -y \
    build-essential libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# 2. Copy dependency manifests (changes with each dependency update)
COPY requirements.txt .

# 3. Install Python dependencies (cached if requirements.txt unchanged)
RUN pip install --no-cache-dir -r requirements.txt

# 4. Copy source code (changes most frequently)
COPY . .

This ordering means dependency installation only reruns when the manifest changes. Editing application code alone will not invalidate the dependency cache layer. For projects with many dependencies, this reduces build time from minutes to seconds on rebuilds. The Docker Build Cache documentation describes this as the most impactful optimization for CI/CD pipelines.

Combining RUN Commands

Each RUN instruction creates a layer. Combine related commands to reduce layer count:

# Bad — 3 layers, cache files in intermediate layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# Good — 1 layer, no cache files
RUN apt-get update && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*

Always clean up in the same RUN instruction. If you clean in a separate layer, the cache files still exist in the intermediate layer and contribute to image size.

.dockerignore and Build Context Optimization

Prevent unnecessary files from being sent to the Docker daemon:

# .dockerignore
.git
__pycache__/
*.pyc
.env
node_modules/
dist/
*.md
tests/

This speeds up builds and prevents secrets from leaking into images. Docker’s build context, defined by the last argument to docker build (usually .), is tarred and sent to the daemon. Excluding unnecessary files dramatically reduces context size and build time. A typical project can reduce context size from 50 MB to under 1 MB with a well-configured .dockerignore.

Security Best Practices

Run as Non-Root

RUN addgroup --system app && adduser --system --ingroup app app
USER app

Running as root inside a container escalates privilege escalation risks. If an attacker gains code execution in a root-running container, they have root on the host via cgroup and namespace escape vulnerabilities. The NSA’s Kubernetes Hardening Guide requires containers to run as non-root.

Security Scanning Integration

docker scout quickview my-image
docker scout recommendations my-image

Set up automated scanning in your CI/CD pipeline to fail builds that introduce critical vulnerabilities. Docker Scout, Snyk Container, and Trivy all integrate with CI platforms and provide vulnerability reports per image layer. The CNCF recommends scanning both base images and final images as part of the build pipeline.

FAQ

How much can multi-stage builds reduce image size?

Typically 80-95%. A Node.js application using node:20 as base (~300 MB with dev dependencies) can be reduced to ~25 MB using an nginx:alpine runtime stage. For Go applications, final images as small as 5-15 MB are achievable using scratch or distroless base images.

Should I use Alpine or Slim as a base image?

Slim (Debian-based) is more compatible because it uses glibc, which most Linux software targets. Alpine uses musl libc, which can cause compatibility issues with Python wheels, Java JNI libraries, and some C extensions. Use Alpine when compatibility is confirmed; use Slim when in doubt.

How do I find what is bloating my image?

Use docker history --human my-image to see layer sizes and docker inspect to see the image manifest. For detailed analysis, use dive (an open-source tool) or docker scout to visualize layer contents and identify space hogs.

Does BuildKit improve image optimization?

Yes. BuildKit enables parallel stage building, cache mounts, and --mount=type=cache for package manager caches. Enable it with DOCKER_BUILDKIT=1. BuildKit’s --secret feature lets you mount secrets at build time without them persisting in image layers — critical for private package registries.

Can I compress Docker images?

Docker images are already compressed when pushed to a registry (gzip compression). You cannot further compress individual images, but you can use docker save with gzip for backup exports. For deployment, image size matters most for pull time — smaller base images and multi-stage builds are the primary levers.

Conclusion

Optimizing Docker images is a continuous improvement process. Start with a specific base image (pin the digest), use multi-stage builds to separate build and runtime, order Dockerfile instructions for caching, clean up package manager artifacts, and always run as a non-root user. These practices produce images that are 80-95% smaller, build faster, and have fewer vulnerabilities — making deployments more efficient and infrastructure costs lower. Start with our Docker beginner’s guide and learn about Docker Compose for multi-service applications.

Advanced Image Size Reduction Techniques

Beyond basic multi-stage builds, several techniques shrink images further. The docker-slim tool analyzes your application and creates a minimal image containing only the necessary files and libraries. Distroless images (gcr.io/distroless/base) contain only your application and its runtime dependencies — no shell, package manager, or utilities — reducing the attack surface significantly. Base image choice matters: alpine:3.19 is ~5 MB vs debian:bookworm-slim at ~80 MB. However, Alpine uses musl libc, which can cause compatibility issues with Python wheels and C extensions compiled against glibc. Layer squashing (docker build --squash) merges all layers into one, removing intermediate files that were deleted in later layers but still occupy space. Analyze image layers with docker history <image> and dive for a visual breakdown. Remove unnecessary files in the same RUN command that created them: apt-get update && apt-get install -y pkg && rm -rf /var/lib/apt/lists/*. Compress assets: use gzip --best for static files and optimizers for images.

Security-Focused Image Optimization

Smaller images are inherently more secure because they have a smaller attack surface. Remove setuid/setgid binaries: find / -perm /6000 -type f -exec chmod a-s {} \;. Add a non-root user and use USER appuser. Set filesystem to read-only in Kubernetes with readOnlyRootFilesystem: true. Sign images with Docker Content Trust or cosign to verify integrity at deploy time. Scan images in CI with Trivy, Grype, or Snyk and fail the build on high-severity CVEs.

Section: Docker 1645 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top