Docker 20 🐳 Caching Mechanisms in Docker Builds: Layer Reuse and Cache Busting Strategies
Docker’s build cache is the difference between a build that takes ten seconds and one that takes ten minutes. The cache works at the layer level: each instruction in a Dockerfile produces a layer, and Docker reuses a layer if the instruction and its inputs have not changed. The cache is what makes iterative development fast, and it is what makes CI pipelines economical. But the cache is also a source of confusion: a build that should be fast is slow because a layer was invalidated too early, or an image is stale because the cache was never broken. Understanding how cache invalidation works — and how to break it deliberately — is the core skill of Dockerfile optimization.
The cache rules are simple once internalized. For RUN instructions, the cache key is the command string itself. For COPY and ADD instructions, the cache key is a checksum of the file metadata for the copied files. Once a layer is invalidated, every subsequent layer is invalidated too, because they depend on the state produced by the earlier layers . This cascade is why instruction order matters so much: putting frequently-changing files at the end of the Dockerfile limits the blast radius of a cache miss.
The strategies for working with the cache fall into three categories. Layer ordering places infrequently-changing instructions first and frequently-changing ones last. Cache mounts persist package manager caches across builds, so even a cache miss does not force a full re-download . External caches export the build cache to a remote location, so CI runners that are torn down between builds can still reuse the cache . This chapter covers all three, along with the deliberate cache-busting techniques that force a rebuild when freshness matters more than speed.
Key point: Docker reuses a layer if the instruction and its inputs are unchanged. For COPY and ADD, the inputs are the file metadata checksum. For RUN, the input is the command string. Once a layer is invalidated, all subsequent layers rebuild. Order instructions from least-frequently-changed to most-frequently-changed, use BuildKit cache mounts to persist package manager caches, and use --no-cache-filter or --cache-bust arguments for deliberate invalidation.
Why caching mechanisms matter
The speed problem. A Node.js build that downloads 800 dependencies takes minutes on a cold cache. On a warm cache, the same build takes seconds. The difference compounds across every build in a development session and every pipeline run in CI. Caching is not an optimization; it is the difference between a usable workflow and an unusable one.
The cascade problem. Cache invalidation is not localized. When one layer misses, every layer after it misses too. A Dockerfile that copies the entire source tree before installing dependencies forces a full dependency reinstall on every code change . The single most impactful optimization in a typical Dockerfile is reordering the instructions so that the dependency manifest is copied first, the dependencies are installed, and only then is the source code copied .
The staleness problem. A cached layer can be stale. A RUN apt-get update that is cached will not re-fetch the package index, so the build will install old versions even when newer ones exist . A base image that is cached will not pick up security patches . The cache is a performance feature, but it is also a freshness liability. Deliberate cache busting is the countermeasure.
The CI problem. CI runners are often ephemeral. Each build starts with an empty local cache, so the first build in every pipeline is cold . External caches solve this by exporting the cache to a registry, a local directory, or a CI-specific backend like the GitHub Actions cache .
The security problem. The build cache can hide vulnerabilities. An image that is never rebuilt never picks up patched base layers. Scanning needs to run against images already in the registry, and rebuild cadence needs to be frequent enough that a scan finding results in a new image . The cache is not just a speed concern; it is a security concern.
a. How layer caching works
Docker builds an image layer by layer, in the order the instructions appear. For each instruction, the builder checks whether it can reuse a cached layer from a previous build .
For RUN instructions, the cache key is the command string. If the string is identical, the layer is reused . The builder does not inspect the container’s filesystem to determine whether the command produced a different result. RUN apt-get update is cached based on the string alone, so it will not re-execute even if the package index has changed upstream .
For COPY and ADD instructions, the cache key is a checksum of the file metadata for the files being copied. The metadata includes content and properties like permissions, but not the modification time (mtime) . If the content or permissions of any copied file change, the layer is invalidated.
For WORKDIR, the cache respects the SOURCE_DATE_EPOCH build argument. Setting it to a dynamic value invalidates the WORKDIR layer and everything after it .
The cascade rule is the critical one: once a layer is invalidated, every subsequent layer is invalidated too . This is why the order of instructions determines the build’s sensitivity to change. If a frequently-changing COPY appears early, every layer after it rebuilds on every commit. If it appears late, only the layers after it rebuild.
b. Optimizing instruction order
The rule for ordering is: least-frequently-changed first, most-frequently-changed last . The base image changes rarely. System packages change occasionally. Application dependencies change when the manifest changes. Application code changes on every commit.
A Dockerfile that follows the rule:
FROM node:20-alpine
WORKDIR /app
# Rarely changes: system packages
RUN apk add --no-cache curl
# Changes when dependencies change: manifest first, then install
COPY package.json package-lock.json ./
RUN npm ci
# Changes frequently: source code
COPY . .
RUN npm run build
A Dockerfile that violates the rule:
FROM node:20-alpine
WORKDIR /app
COPY . . # ❌ copies source before dependencies
RUN npm ci # ❌ re-runs on every code change
RUN npm run build
The first version installs dependencies only when package.json or package-lock.json changes. The second version reinstalls them on every source change. For a project with a large dependency tree, the difference is minutes per build .
The same principle applies to system packages. RUN apt-get update && apt-get install -y ... should be combined into a single instruction so the package index and the installation are in the same layer . If they are separate, the index layer can be cached while the installation layer misses, producing a stale index and a failed or outdated installation.
c. BuildKit cache mounts
A cache mount persists a directory across builds without including it in the image layer. It is the tool for package manager caches, which are large and change incrementally .
The syntax uses RUN --mount=type=cache,target=<path>:
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
The # syntax=docker/dockerfile:1 directive is required as the first line of the Dockerfile when using cache mounts . BuildKit must be enabled; it is the default in Docker Desktop and Docker Engine 23.0+ .
The cache mount solves a specific problem. When package.json changes, the RUN npm ci layer is invalidated and must re-execute. Without the cache mount, the re-execution downloads every package from the registry. With the cache mount, npm checks its persistent cache first, and only new or updated packages are downloaded .
The cache mount directories for common package managers :
| Package Manager | Cache Target | Notes |
|---|---|---|
| npm | /root/.npm | Works with npm ci and npm install |
| Yarn Classic | /root/.yarn | Set YARN_CACHE_FOLDER=/root/.yarn |
| Yarn Berry | /root/.yarn/berry/cache | Use --immutable |
| pnpm | /root/.local/share/pnpm/store | Content-addressable store |
| pip | /root/.cache/pip | Remove --no-cache-dir |
| apt | /var/cache/apt + /var/lib/apt | Use sharing=locked |
| Go modules | /go/pkg/mod | Also cache /root/.cache/go-build |
For apt, the sharing=locked option is required because apt needs exclusive access to its cache files. Parallel builds using the same cache mount will wait for each other rather than corrupt the cache .
A significant benefit of cache mounts is that they eliminate the need for cleanup commands. With a regular layer, rm -rf /var/lib/apt/lists/* is needed to keep the image small. With a cache mount, the cache directory is not part of the layer, so there is nothing to clean up .
d. External cache backends
The default cache is internal to the BuildKit instance. In CI, where the builder is often ephemeral, that cache is lost between runs. External caches export the build cache to a remote location so it can be reused .
The two flags are --cache-to (export) and --cache-from (import):
docker buildx build \
--cache-to type=registry,ref=registry.example.com/app:cache \
--cache-from type=registry,ref=registry.example.com/app:cache \
--push -t registry.example.com/app:latest .
The backends include :
| Backend | Description |
|---|---|
inline | Embeds cache in the image; works only with the image exporter |
registry | Exports cache to a separate image in a registry |
local | Writes cache to a local directory |
gha | Uploads cache to GitHub Actions cache |
s3 | Uploads cache to an AWS S3 bucket |
The mode parameter controls how much is exported. mode=min (the default) exports only the layers in the final image. mode=max exports all layers, including intermediate steps, which produces more cache hits at the cost of larger exports .
For CI, the gha backend is convenient for GitHub Actions workflows, and the registry backend is portable across CI providers. A common pattern is to import from both the current branch and the main branch .
e. Cache busting strategies
Sometimes the cache must be broken deliberately. The reasons include refreshing a base image to pick up security patches, forcing a dependency reinstall after a registry change, or ensuring that a RUN apt-get update actually fetches the current index .
Build argument cache busting is the most portable technique. An ARG that is not used in any instruction does not affect the cache, but an ARG that appears in a RUN command changes the command string and invalidates the layer and everything after it .
ARG CACHE_BUST=1
RUN echo "bust: ${CACHE_BUST}" && npm ci
At build time:
docker build --build-arg CACHE_BUST=$(date +%s) -t myapp .
The date +%s produces a new value on every build, so the RUN layer and everything after it rebuilds . The layers before the ARG usage remain cached.
--no-cache-filter invalidates a specific build stage without invalidating the entire build:
docker build --no-cache-filter install .
This is useful when the Dockerfile uses multi-stage builds and only one stage needs refreshing .
--pull forces the builder to check for a newer version of the base image:
docker build --pull -t myapp .
Without --pull, a cached base image layer is reused even if the upstream tag points to a newer image . This is the mechanism for picking up security patches in the base image.
Scheduled rebuilds with --no-cache on a cadence (weekly, for example) ensure that stale base layers are eventually refreshed, independent of code changes .
f. The security and freshness trade-off
Aggressive caching optimizes for speed. Aggressive rebuilding optimizes for freshness. The right answer is usually different cadences for different layers .
Dependency and OS-package layers should be rebuilt periodically even without a code change, because their vulnerability exposure changes when upstream packages are patched. Application code layers should rebuild on every commit, because that is the layer the developer controls and wants fast feedback on.
Multi-stage builds help reconcile the two. A builder stage can cache aggressively for speed, while the final runtime stage only ships what is needed, keeping the freshness problem scoped to a smaller surface . The runtime stage’s base image is the one that needs periodic refreshing; the builder stage’s cache can be allowed to persist longer.
Complete Example Session
# ============================================
# PART 1: UNOPTIMIZED DOCKERFILE
# ============================================
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
CMD ["node", "dist/server.js"]
# Every code change re-runs npm ci
# ============================================
# PART 2: REORDERED FOR CACHING
# ============================================
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]
# npm ci runs only when package.json changes
# ============================================
# PART 3: ADD CACHE MOUNT FOR NPM
# ============================================
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]
# ============================================
# PART 4: CACHE MOUNT FOR APT
# ============================================
# syntax=docker/dockerfile:1
FROM debian:12-slim
RUN rm -f /etc/apt/apt.conf.d/docker-clean && \
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends curl
# ============================================
# PART 5: CACHE MOUNT FOR PIP
# ============================================
# syntax=docker/dockerfile:1
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
# ============================================
# PART 6: EXTERNAL CACHE WITH REGISTRY
# ============================================
docker buildx build \
--cache-to type=registry,ref=registry.example.com/app:cache,mode=max \
--cache-from type=registry,ref=registry.example.com/app:cache \
-t registry.example.com/app:latest .
# ============================================
# PART 7: CACHE BUSTING WITH ARG
# ============================================
docker build --build-arg CACHE_BUST=$(date +%s) -t myapp .
# ============================================
# PART 8: CACHE BUST IN DOCKERFILE
# ============================================
ARG CACHE_BUST=1
RUN echo "bust: ${CACHE_BUST}" && apt-get update
# ============================================
# PART 9: FORCE BASE IMAGE REFRESH
# ============================================
docker build --pull -t myapp .
# ============================================
# PART 10: INVALIDATE A SPECIFIC STAGE
# ============================================
docker build --no-cache-filter install .
These ten parts cover the unoptimized Dockerfile, the reordered version, cache mounts for npm, apt, and pip, external caching with a registry backend, cache busting with an argument, the Dockerfile syntax for cache busting, forcing a base image refresh, and invalidating a specific build stage.
Quick Reference
Cache Invalidation Rules
| Instruction | Cache Key |
|---|---|
RUN | Command string only |
COPY / ADD | File metadata checksum (not mtime) |
WORKDIR | SOURCE_DATE_EPOCH |
| Cascade | Any miss invalidates all subsequent layers |
Instruction Order
| Order | Change Frequency |
|---|---|
| Base image | Rarely |
| System packages | Occasionally |
| Dependency manifest | When dependencies change |
| Source code | Every commit |
Cache Mount Targets
| Package Manager | Target | Notes |
|---|---|---|
| npm | /root/.npm | Works with npm ci |
| pip | /root/.cache/pip | Remove --no-cache-dir |
| apt | /var/cache/apt + /var/lib/apt | sharing=locked |
| Go | /go/pkg/mod | Also cache build cache |
| pnpm | /root/.local/share/pnpm/store | Content-addressable |
External Cache Backends
| Backend | Use Case |
|---|---|
registry | Portable across CI providers |
gha | GitHub Actions |
local | Local directory |
inline | Single image, simple setups |
Cache Busting
| Technique | Scope |
|---|---|
--build-arg CACHE_BUST | From the ARG usage onward |
--no-cache-filter <stage> | One build stage |
--pull | Base image layer |
--no-cache | Entire build |
| Scheduled rebuild | All layers |
Best Practices
✅ Do This:
# Copy manifest before source
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# Use cache mounts for package managers
RUN --mount=type=cache,target=/root/.npm npm ci
# Use sharing=locked for apt
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y curl
# Export and import external cache in CI
--cache-to type=registry,ref=... --cache-from type=registry,ref=...
❌ Don’t Do This:
# Copy source before dependencies
COPY . . # ❌ invalidates npm ci on every code change
RUN npm ci
# Separate apt update and install
RUN apt-get update # ❌ cached index may be stale
RUN apt-get install -y curl
# Use --no-cache on every build
docker build --no-cache . # ❌ wastes time on unchanged layers
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Slow rebuilds | Source copied before dependencies | Copy manifest first |
| Stale packages | RUN apt-get update cached | Use --no-cache-filter or cache bust |
| Cache not reused in CI | Ephemeral builder | Use --cache-from with external backend |
mtime invalidates cache | Assumed it matters | It does not; content and permissions do |
| Cache mount ignored | No syntax directive | Add # syntax=docker/dockerfile:1 |
| Base image stale | --pull not used | Rebuild with --pull periodically |
Real-World Examples
1. Node.js Optimized Build
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY . .
RUN npm run build
2. Python Optimized Build
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
COPY . .
3. Go Optimized Build
FROM golang:1.22
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build go build -o /app
4. External Cache in GitHub Actions
- uses: docker/build-push-action@v7
with:
cache-from: type=gha
cache-to: type=gha,mode=max
5. Cache Busting on Base Image
docker build --pull --no-cache-filter install -t myapp .
6. Multi-Stage with Cached Builder
FROM golang:1.22 AS builder
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN go build -o /app
FROM alpine:3.19
COPY --from=builder /app /app
CMD ["/app"]
7. Cache Mount with Sharing Lock
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
apt-get update && apt-get install -y curl
8. Registry Cache Export
docker buildx build --cache-to type=registry,ref=app:cache,mode=max .
9. Cache Bust Argument
docker build --build-arg CACHE_BUST=$(date +%s) .
10. Verify Cache Usage
docker build . 2>&1 | grep "CACHED"
Visual
Cache Invalidation Cascade
┌──────────────────────────────────────────────────────────────┐
│ LAYER 1: FROM node:20-alpine CACHED │
│ LAYER 2: RUN apk add curl CACHED │
│ LAYER 3: COPY package.json CACHED │
│ LAYER 4: RUN npm ci CACHED │
│ LAYER 5: COPY . . MISS ← source changed │
│ LAYER 6: RUN npm run build REBUILD ← cascade │
│ │
│ When layer 5 misses, layer 6 rebuilds even though its │
│ command did not change. │
└──────────────────────────────────────────────────────────────┘
Optimal Instruction Order
┌──────────────────────────────────────────────────────────────┐
│ LEAST FREQUENTLY CHANGED (top, cache longest) │
│ ├── FROM base:tag │
│ ├── RUN system packages │
│ ├── COPY dependency manifest │
│ ├── RUN install dependencies │
│ ├── COPY source code │
│ └── RUN build │
│ MOST FREQUENTLY CHANGED (bottom, cache shortest) │
└──────────────────────────────────────────────────────────────┘
Cache Mount vs Regular Layer
┌──────────────────────────────────────────────────────────────┐
│ REGULAR LAYER: │
│ RUN npm ci │
│ └── Downloads packages into the image layer. │
│ └── If layer misses, re-downloads everything. │
│ └── Packages are part of the image. │
│ │
│ CACHE MOUNT: │
│ RUN --mount=type=cache,target=/root/.npm npm ci │
│ └── Downloads packages into a persistent cache. │
│ └── If layer misses, only new packages are downloaded. │
│ └── Cache is not part of the image. │
└──────────────────────────────────────────────────────────────┘
External Cache Flow
┌──────────────────────────────────────────────────────────────┐
│ CI RUNNER 1: │
│ docker buildx build --cache-to type=registry,ref=app:cache │
│ │ │
│ ▼ │
│ REGISTRY: app:cache │
│ │ │
│ ▼ │
│ CI RUNNER 2: │
│ docker buildx build --cache-from type=registry,ref=app:cache│
│ └── Reuses cache from runner 1 despite being ephemeral │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Cache invalidation | Instruction + inputs must match |
RUN cache key | Command string |
COPY/ADD cache key | File metadata checksum |
| Cascade | Miss invalidates all subsequent layers |
| Optimal order | Least-frequently-changed first |
| Cache mount | RUN --mount=type=cache,target=... |
| npm cache target | /root/.npm |
| pip cache target | /root/.cache/pip |
| apt cache target | /var/cache/apt + /var/lib/apt |
| External cache flags | --cache-to and --cache-from |
| Cache busting | --build-arg CACHE_BUST |
| Force base refresh | --pull |
Key takeaways:
- Docker reuses a layer when the instruction and its inputs are unchanged. For
RUN, the input is the command string. ForCOPYandADD, the input is a checksum of the file metadata. Modification time does not participate in the checksum . - Cache invalidation cascades. Once one layer misses, every layer after it rebuilds. This is why instruction order determines the build’s sensitivity to change .
- Order instructions from least-frequently-changed to most-frequently-changed. Base image first, system packages second, dependency manifest third, source code last. This limits the cascade to the layers that need to rebuild .
- Cache mounts persist package manager caches across builds. They solve the problem of re-downloading everything when the dependency layer is invalidated. The cache is not part of the image, so the image stays small .
- External caches export the build cache for CI. The
--cache-toand--cache-fromflags with a registry or CI-specific backend let ephemeral runners reuse cache from previous builds . - Cache busting is deliberate invalidation. Use a build argument with a changing value,
--no-cache-filterfor a specific stage, or--pullfor the base image . - Caching has a security dimension. A stale base image does not pick up security patches. Periodic rebuilds with
--pullor scheduled--no-cacheensure freshness .
Remember: The build cache is a performance feature, but it is also a correctness and security concern. The layer cache reuses instructions that have not changed, and the cascade rule means that a single miss forces a rebuild of everything after it. The two most impactful optimizations are reordering instructions so that frequently-changing files come last, and using cache mounts so that package manager caches persist across builds. External caches extend the benefit to CI. And deliberate cache busting — through build arguments, --no-cache-filter, or --pull — ensures that the cache does not hide staleness. The goal is not to cache everything or rebuild everything, but to cache the layers that change rarely and rebuild the layers that change often.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!