What is Docker Layer Caching?
Every Docker image is built from a series of instructions in a Dockerfile. Each instruction—like FROM, RUN, COPY, or ADD—creates a new layer in the final image. These layers are stacked on top of each other and are immutable. Docker's build system can reuse these layers if they haven't changed, which is the core idea behind layer caching.
When you run docker build a second time, Docker checks each instruction against its cache. If the instruction and its context (the files or commands it depends on) are identical to a previous build, Docker reuses the cached layer instead of executing the instruction again. This dramatically reduces build time and ensures consistent, reproducible images.
How caching works under the hood
Docker uses a content-addressable cache key for each layer. The key is generated from:
- The instruction itself (e.g.,
RUN npm install) - The base image layer (parent layer hash)
- For
COPYandADD, a checksum of all files being copied - For
ARG/ENVthat affect the build, the values used
If any part of the key changes, the cache is invalidated for that layer, and all subsequent layers must be rebuilt—even if they haven't changed logically. This cascading invalidation is the most important behavior to understand when designing a Dockerfile for optimal caching.
Why Layer Caching Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Efficient layer caching is not just about saving a few seconds locally. It impacts:
- Development iteration speed – Developers rebuild images constantly. A cache miss can turn a 2-second rebuild into a 2-minute one.
- CI/CD pipeline performance – In continuous integration, minutes saved per pipeline run translate directly into cost reduction and faster feedback loops.
- Reliability and consistency – Reusing layers avoids network flakiness (e.g., failed package downloads) because the layer is already stored.
- Bandwidth and storage – Pulling only changed layers instead of entire images saves network traffic and disk space.
A poorly ordered Dockerfile can cause a full rebuild for a trivial change, while a well-optimized one can complete in milliseconds. Understanding layer caching is therefore a fundamental skill for anyone working with containers in production.
How to Use Layer Caching Effectively
The golden rule: place instructions that change frequently later in the Dockerfile, and keep stable, slow-to-execute instructions early. This maximizes cache hits for expensive steps like dependency installation while isolating fast-changing application code to the final layers.
Example: A naive Node.js Dockerfile
Consider this common anti-pattern:
# Bad: application code copied before npm install
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
In this example, any change to any file in the project (including a single line in a markdown file) will invalidate the COPY . . layer. That forces RUN npm install to execute again from scratch, downloading all dependencies every time. This is painful for developers.
Optimized version
Separate dependency installation from application code:
# Good: package.json and lock file copied first
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]
Now, changes to source files only bust the cache for the final COPY layer, leaving the npm ci layer intact. The dependency install step is reused until package.json or the lock file changes. This is a massive improvement.
Multi-stage builds and caching
Multi-stage builds allow you to compile or prepare artifacts in one stage and copy only the necessary output into a lean final image. Each stage has its own independent cache. By structuring stages carefully, you can cache intermediate build tools and avoid shipping them.
# Multi-stage Go build with cached dependencies
FROM golang:1.21 AS builder
WORKDIR /src
# Copy dependency definitions first
COPY go.mod go.sum ./
RUN go mod download
# Then copy source code
COPY . .
RUN go build -o /app/server .
# Final minimal image
FROM alpine:3.18
COPY --from=builder /app/server /usr/local/bin/server
CMD ["server"]
Here, the go mod download step is cached until go.mod or go.sum change. The source code changes only rebuild the compilation step, not the dependency download.
Best Practices for Docker Layer Caching
1. Order instructions by stability
Always put the least frequently changed instructions at the top. Typical order from top to bottom:
- Base image (
FROM) - System package updates and installs (
apt-get,yum,apk) - Language runtime dependencies (pip, gem, npm, maven)
- Application dependencies (lock file copy + install)
- Application source code
- Final configuration or secrets (with care)
2. Combine RUN instructions and clean up in the same layer
Each RUN creates a separate layer. Merging related commands into a single RUN reduces layer count and ensures cleanup doesn't bloat the final image. The cache will treat the entire combined command as one unit.
# Good: update, install, and clean in one layer
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
ca-certificates && \
rm -rf /var/lib/apt/lists/*
Doing update, install, and delete in separate RUN lines would leave the package cache in an intermediate layer, wasting space and making caching less predictable.
3. Copy specific files, not entire directories
Instead of COPY . . early, copy only the files that are needed for the next step. Use wildcards and multiple COPY directives to precisely control cache invalidation.
# Precisely copy dependency manifests
COPY package.json package-lock.json ./
RUN npm ci
# Now copy application code
COPY src/ ./src/
COPY public/ ./public/
If your project has many files that don't affect the build (like docs, tests, or local configs), use a .dockerignore file to exclude them. This not only speeds up the build context transfer but also prevents unnecessary cache busting.
4. Leverage --mount=type=cache for package managers
Modern Docker (BuildKit) supports cache mounts that persist across builds even when the layer itself is rebuilt. This is extremely powerful for package manager caches (like npm, pip, apt). Instead of relying solely on layer caching, you can use:
# Using cache mount for npm (requires BuildKit)
RUN --mount=type=cache,target=/root/.npm \
npm ci
Even when package.json changes, the npm cache directory is preserved, so unchanged packages are not redownloaded. This can cut installation time by an order of magnitude.
5. Be deliberate with ARG and ENV
Environment variables used in a RUN instruction become part of the cache key. If you pass a build argument like --build-arg VERSION=1.2.3, it will bust the cache for any RUN that references $VERSION. To minimize this:
- Declare
ARGas late as possible, right before it's used. - If an
ARGis used only in a final metadata step, place that step last. - Consider using multi-stage builds: use the
ARGin a late stage to avoid invalidating the heavy build stage.
# ARG only used in final image metadata, after the heavy lifting
FROM builder AS build
# ... heavy compilation steps that don't use VERSION
FROM alpine:3.18
ARG VERSION
LABEL version=${VERSION}
COPY --from=build /app/server /usr/local/bin/server
6. Use .dockerignore aggressively
The build context is sent to the Docker daemon before any caching logic runs. If your context contains thousands of unnecessary files (node_modules, .git, logs, IDE settings), the checksum calculation for COPY can be slow and cache-invalidating. A well-crafted .dockerignore file ensures only essential files are considered.
# Example .dockerignore
node_modules
.git
*.log
Dockerfile
docker-compose.yml
README.md
Common Pitfalls
Pitfall 1: ADD instead of COPY
ADD has extra features like URL fetching and tar auto-extraction. These break cache predictability because a URL might return different content even if the Dockerfile instruction is unchanged. Always prefer COPY unless you specifically need ADD's extra functionality. COPY is explicit and deterministic.
Pitfall 2: Accidentally busting the cache with dynamic content
Commands that fetch the current time, random numbers, or external data will always produce a different layer and invalidate the cache. For example:
# Bad: curl always fetches fresh data, busting cache
RUN curl -o /etc/motd https://example.com/motd.txt
If you must include external data, consider fetching it at container startup (via an entrypoint script) instead of during build, or use cache mounts and a pinned version.
Pitfall 3: Running apt-get update and apt-get install in separate layers
This is a classic mistake:
RUN apt-get update
RUN apt-get install -y nginx
If the first layer is cached (because the apt-get update command itself hasn't changed), you might be installing packages from an outdated package index. Always combine them in one RUN with cleanup.
Pitfall 4: Using ARG before FROM without understanding cache behavior
ARG before the first FROM is special: it's used only in FROM lines and does not persist. If you try to use it later without redeclaring, it's empty, leading to confusing cache breaks. Always redeclare ARG after FROM if you need it in later stages.
Pitfall 5: Relying on cache in CI without a proper cache backend
Local builds benefit from the local layer cache stored on disk. In ephemeral CI environments (like GitHub Actions or GitLab CI), each job may start without any cache. Without a remote cache (using --cache-from with a registry, or Docker BuildKit's inline cache), every build starts from scratch. Use:
# Example: using cache-from in CI
docker build --cache-from myapp:latest --tag myapp:new .
Modern solutions like docker buildx build --cache-to=type=registry push cache layers to a remote registry, making CI builds consistently fast.
Pitfall 6: Copying everything then filtering with .dockerignore incorrectly
.dockerignore follows a different syntax than .gitignore. A common mistake is using node_modules/ instead of node_modules (trailing slash matters). Always test what's included by running docker build --no-cache -t tmp . and inspecting, or use docker build -f Dockerfile --print-build-context with modern Docker.
Conclusion
Docker layer caching is one of the most impactful mechanisms for speeding up container builds and ensuring efficient CI/CD workflows. The key to mastering it is understanding cache invalidation: once a layer changes, everything below it must be rebuilt. By carefully ordering instructions, separating dependency installation from application code, using multi-stage builds, leveraging BuildKit cache mounts, and avoiding common pitfalls like dynamic commands or poorly scoped file copies, you can achieve near-instant rebuilds for everyday changes. Treat your Dockerfile as a blueprint for caching—each line is a decision that either preserves or destroys the cache. With the practices outlined here, you'll build faster, ship sooner, and spend less time staring at progress bars.