Hop Into Eggciting Learning Opportunities | Flat 25% OFF | Code: EASTER
docker6 min read

Optimizing Dockerfiles: Layer Caching, Multi-Stage Builds, and Smaller Image Sizes

Suyash RaizadaSuyash Raizada
Optimizing Dockerfiles: Layer Caching, Multi-Stage Builds, and Smaller Image Sizes

Optimizing Dockerfiles is one of the highest-leverage improvements you can make in containerized environments. Better Dockerfile practices directly improve build speed, reduce CI/CD compute usage, cut storage and bandwidth costs, and shrink the attack surface by removing unnecessary packages and tools. With container images accumulating more dependencies over time, minimalism and repeatable builds are no longer optional. Sysdig's 2025 Cloud-Native Security Report highlighted a 300% increase in packages per container image, reinforcing why teams should prioritize smaller, cleaner images and tighter build discipline.

This guide covers three pillars of Dockerfile optimization: layer caching, multi-stage builds, and smaller image sizes, along with modern tooling and best practices you can standardize across teams.

Certified Artificial Intelligence Expert Ad Strip

Why Optimizing Dockerfiles Matters

In real-world DevOps and platform engineering, Dockerfiles are part of your software supply chain. Optimizing them helps you:

  • Build faster: Cache hits and smaller contexts reduce rebuild time, especially in CI pipelines.

  • Use fewer resources: Smaller layers and fewer packages lower CPU, memory, and disk usage in pipelines and registries.

  • Improve security: Fewer packages and build tools mean fewer vulnerabilities and a smaller exposed surface area.

  • Reduce costs: Less storage, less egress, and shorter pipeline runtimes often translate to direct savings.

Docker's official guidance stresses optimizing layer order for cache hits and using multi-stage builds to separate build-time and runtime concerns. Cloud-native industry recommendations also increasingly emphasize pinned versions, minimal base images, and automated linting.

Layer Caching Optimization: Get More Cache Hits

Docker builds images as a sequence of layers. Each Dockerfile instruction typically creates a new layer. When Docker rebuilds, it reuses cached layers if the instruction and its inputs are unchanged. The practical goal is straightforward: maximize cache reuse for the most expensive steps - dependency installs and compilation - by ordering instructions strategically.

1) Order Layers by Change Frequency

Place the most stable steps first and the most frequently changing steps last. A common pattern is copying dependency manifests before copying the application source. This keeps dependency install layers cached unless the dependencies themselves change.

Node.js example (cache-friendly ordering)

FROM node:20-slim
WORKDIR /app

# Copy only dependency manifests first
COPY package.json package-lock.json ./
RUN npm ci

# Copy the rest of the source code
COPY . .

RUN npm run build
CMD ["node", "dist/server.js"]

This pattern prevents a small code change from invalidating the dependency install cache, which is one of the most common causes of unnecessarily slow builds.

2) Combine Related RUN Commands to Reduce Layers

Each RUN instruction creates a layer. Excess layers increase image size and can slow builds. Combine compatible commands with &&, and clean temporary files in the same layer so they do not persist into the final image.

Example (Debian/Ubuntu-based images)

RUN apt-get update \
  && apt-get install -y --no-install-recommends curl ca-certificates \
  && rm -rf /var/lib/apt/lists/*

Cleaning in the same layer is essential. If you clean in a later layer, the earlier layer still contains the cached package lists, and that data remains part of the image.

3) Use .dockerignore to Shrink Build Context

Large build contexts slow down builds because Docker must send the entire context to the daemon or remote builder. A well-maintained .dockerignore file reduces transfer time and prevents accidental inclusion of local files in images.

Typical .dockerignore entries

node_modules
.git
Dockerfile
.dockerignore
*.log
.env
dist

Right-sizing the build context is one of the simplest ways to speed up builds, particularly when teams work with monorepos.

Multi-Stage Builds: Keep Runtime Images Slim and Secure

Multi-stage builds split your Dockerfile into multiple stages. You compile or build artifacts in a stage that has all the heavy tooling, then copy only the final outputs into a clean runtime stage. This removes compilers, package managers, and build dependencies from production images entirely.

Key benefits include:

  • Smaller images: Only the runtime essentials ship to production.

  • Better security posture: Build tools and unused libraries are excluded from the final image.

  • More maintainable Dockerfiles: Common stages can be reused across services, reducing duplication.

Go Example: Build with golang, Run with scratch or alpine

# Stage 1: build
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app ./cmd/app

# Stage 2: minimal runtime
FROM scratch
COPY --from=builder /src/app /app
ENTRYPOINT ["/app"]

This pattern can dramatically reduce image size because the final stage contains only the compiled binary. Many teams choose scratch for the smallest possible image, or alpine when they need a minimal shell and CA certificates.

Node.js Example: Build Artifacts in One Stage, Run in Another

# Stage 1: build
FROM node:20-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: runtime
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]

Multi-stage builds are especially effective for microservices where build tooling differs significantly from runtime requirements.

Achieving Smaller Image Sizes: Practical Techniques

After addressing caching and multi-stage builds, the next focus is systematically shrinking what you ship. Smaller images typically mean fewer vulnerabilities, faster deployments, and lower storage costs.

1) Choose Minimal Base Images and Pin Versions

Start from a smaller base such as alpine or official *-slim variants - for example, python:3.12-slim instead of ubuntu:latest. Pin versions to improve reproducibility and avoid unexpected changes when upstream tags are updated.

  • Preferred: python:3.12-slim, node:20-slim, alpine:3.20

  • Avoid in production: ubuntu:latest for simple apps, unpinned tags

2) Avoid Unnecessary Packages and Clean Caches

Install only what you need. Use flags like --no-install-recommends on Debian-based images. Clean package caches and temporary build files within the same layer to ensure they do not persist.

For Python:

RUN pip install --no-cache-dir -r requirements.txt

For Debian/Ubuntu:

RUN apt-get update \
  && apt-get install -y --no-install-recommends build-essential \
  && rm -rf /var/lib/apt/lists/*

3) Create a Non-Root Runtime User

Running containers as a non-root user is a widely accepted security requirement. While it does not directly reduce image size, it is an important part of Dockerfile hardening and limits the potential impact if a container is compromised.

RUN useradd -r -u 10001 appuser
USER 10001

4) Use ARG and ENV for Deterministic Builds

Use ARG for build-time configuration and ENV for runtime configuration. Keep them explicit and never embed secrets in images. Standardizing these patterns supports multiple environments and CI workflows without introducing ambiguity.

Tooling That Helps Enforce Dockerfile Optimization

Optimization is easier when you can measure, lint, and automate. Commonly used tools include:

  • Hadolint: Lints Dockerfiles against best practices, useful for enforcing organizational standards in CI.

  • Dive: Inspects image layers to identify where size bloat occurs.

  • DockerSlim: Automates image minimization in some pipelines by removing unnecessary artifacts.

  • BuildKit and Buildx: Improve build performance and support parallelism and advanced caching strategies.

These tools also support enterprise use cases where many microservices must follow consistent, auditable patterns.

Practical Optimization Checklist

  1. Reorder instructions to maximize cache hits - copy manifests first, then application source.

  2. Use .dockerignore to reduce build context and prevent unwanted files from entering the image.

  3. Combine RUN commands and clean caches within the same layer.

  4. Adopt multi-stage builds to remove build tooling from runtime images.

  5. Choose minimal base images and pin versions for reproducible builds.

  6. Run as non-root and keep runtime images minimal for security.

  7. Automate checks with Hadolint and validate layer size with Dive.

Conclusion

Optimizing Dockerfiles through layer caching, multi-stage builds, and smaller image sizes is one of the most effective ways to improve delivery speed, reduce infrastructure waste, and strengthen container security. As images accumulate more packages over time, the discipline of minimal, repeatable builds becomes more important, not less. Start with caching-friendly instruction ordering, adopt multi-stage builds to separate build and runtime concerns, and standardize on minimal base images with automated linting across your CI/CD pipeline.

If your team is formalizing container and cloud-native skills, Blockchain Council offers training paths in DevOps, Cloud Computing, Cybersecurity, and Kubernetes to help engineers align on secure, efficient container delivery practices.

Related Articles

View All

Trending Articles

View All

Search Programs

Search all certifications, exams, live training, e-books and more.