How Docker Knows When to Use the Build Cache

Docker reuses a layer when it already has one built on the same parent layer with the same cache key. For most instructions the key is the instruction text (after build-arg and environment substitution); for COPY and ADD it also includes a checksum of the copied files' contents and permissions, while modification and access times are ignored. RUN is never re-evaluated against the outside world, so an unchanged RUN apt-get update stays cached. Once one step misses the cache, every later step in that stage is rebuilt, because its parent layer is new.

Last reviewed on 2026-09-25

Cache rules per instruction

For every instruction, Docker computes a cache key and looks for a result it already has that was built from the same parent with the same key. On a hit it reuses that result and prints CACHED. On a miss it runs the instruction, and every instruction after it in the stage runs too. What goes into the key depends on the instruction:

InstructionCache key is derived fromWhat breaks it
RUN The command string, the parent layer, and the build args and environment in scope. Editing the command, or changing an ARG/ENV value it can see. The command's output is never checked: RUN apt-get update, RUN curl … and RUN git clone … stay cached even when the remote content has changed.
COPY / ADD (local files) The instruction and a checksum of each source file's contents and metadata such as permissions. Any change to the contents or mode of a copied file, or a file added to or removed from a copied directory. Modification and access times are not part of the checksum, so touch or a fresh git checkout doesn't cause a miss.
COPY --from=<stage> The instruction and the checksum of the files in the source stage. A rebuild of the source stage that produced different files.
FROM The resolved base image (digest). The tag resolving to a new digest, after a docker pull or a build with --pull. Without --pull, a locally present tag is reused as is.
ARG The argument's value, from the first instruction that uses it. Passing a different --build-arg. Every RUN after the ARG sees it as an environment variable, so a new value invalidates those RUN steps even if the command doesn't mention the variable.
ENV, WORKDIR, LABEL, USER, CMD, ENTRYPOINT, EXPOSE The instruction string. Editing the line. These steps are cheap to redo, but a change still invalidates everything below it.

Two consequences explain most "why is my build not cached?" reports:

  • Invalidation cascades downward. Once one step misses, no later step in the same stage can hit, because each key includes its parent. A COPY . . near the top rebuilds the whole file on every source edit, which is why dependency installation belongs above the copy of application code.
  • Docker never re-checks the outside world. It compares instructions and file checksums, not package repositories, APIs or clocks. To pick up fresh upstream content you have to force a rebuild.

BuildKit, the default builder since Docker Engine 23.0, evaluates a dependency graph rather than one chain. Independent stages are cached separately, so a miss in one stage doesn't touch an unrelated one. Within a stage, the cascade rule holds.

What CACHED means in build output

CACHED on a step means BuildKit reused an existing result for that step and didn't run it. The default TTY output collapses finished steps, so use plain progress to see every decision:

docker build --progress=plain -t myapp . 2>&1 | grep -E '^#[0-9]+ (\[|CACHED)'
#5 [2/6] WORKDIR /app
#5 CACHED
#6 [3/6] COPY package.json package-lock.json ./
#6 CACHED
#7 [4/6] RUN npm ci
#7 CACHED
#8 [5/6] COPY . .
#9 [6/6] RUN npm run build

Here the first step without CACHED is COPY . .: a source file changed, so it and everything after it ran. If the first miss is higher up than you expect, the fix is usually to move the instruction that changes often further down, or to exclude the changing files with .dockerignore.

Inspecting and listing the build cache

# List build cache records with size and reclaimable space
docker buildx du

# Per-record details: ID, description (the step), last used, shared
docker buildx du --verbose

# Total build cache size alongside images, containers and volumes
docker system df
  • docker buildx du reports on the current builder. Use --builder <name> for a different one (for example a docker-container builder used in CI).
  • docker history <image> shows an image's layers and the instruction behind each. It does not show whether a step was cached.
  • docker build --check runs Dockerfile lint checks. Despite the name, it doesn't check the cache.

Forcing a rebuild

CommandEffect
docker build --no-cache .Ignores the cache for every step. Combine with --pull to also fetch the newest base images.
docker build --no-cache-filter build,test .BuildKit only. Ignores the cache for the named stages; other stages can still hit.
docker build --pull .Resolves every FROM tag against the registry. If a tag now points to a new digest, that stage misses from FROM down.
--build-arg CACHEBUST=$(date +%s)With ARG CACHEBUST placed directly above the step to refresh, every RUN from there down misses; everything above stays cached.
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates
ARG CACHEBUST=1
RUN apt-get update && apt-get upgrade -y   # re-runs whenever CACHEBUST changes

--no-cache doesn't empty RUN --mount=type=cache directories. Those persist until you prune them (see below).

Clearing the build cache

docker builder prune                      # dangling build cache only
docker builder prune -a                   # all unused build cache
docker builder prune --filter until=72h   # records not used in the last 72 hours
docker buildx prune --builder ci-builder  # a specific buildx builder

Pruning only frees disk space. It doesn't change what a build produces; the next build simply has fewer hits.

Ordering a Dockerfile for cache hits

# Misses on every source edit: COPY . . changes, so npm ci re-runs
FROM node:22-slim
WORKDIR /app
COPY . .
RUN npm ci

# Hits for npm ci unless package.json or the lockfile changes
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

Put what changes least (base image, system packages, dependency manifests) first and what changes most (source code) last. For techniques beyond ordering, see the Docker build caching techniques tutorial. It covers cache mounts (RUN --mount=type=cache), per-language patterns, and sharing the cache in CI with --cache-from/--cache-to.

FAQ

How does Docker know when to use the cache during a build and when not?

Docker reuses a layer when it already has one built on the same parent layer with the same cache key. For most instructions the key is the instruction text (after build-arg and environment substitution); for COPY and ADD it also includes a checksum of the copied files' contents and permissions, while modification and access times are ignored. RUN is never re-evaluated against the outside world, so an unchanged RUN apt-get update stays cached. Once one step misses the cache, every later step in that stage is rebuilt, because its parent layer is new.

What does CACHED mean in docker build output?

CACHED means BuildKit found an existing result for that step with the same parent and cache key, and reused it without running the instruction. Steps without CACHED were executed. Use docker build --progress=plain to see the marker on every step.

Why did my Docker build stop using the cache?

The first step that is not marked CACHED is where the cache broke. Usual causes: a COPY of application code placed above dependency installation, a changed --build-arg value, a base image tag that now resolves to a new digest, a file in the build context that changed because .dockerignore does not exclude it, or cache that was pruned. Run docker build --progress=plain to find the step.

Does Docker re-run RUN apt-get update when packages change upstream?

No. Docker compares the instruction, not the result of running it, so RUN apt-get update stays cached until the line or something above it changes. Combine update and install in one RUN, and to force fresh packages use --no-cache, --no-cache-filter for one stage, or a build argument whose value changes.

Does changing a file's modification time invalidate the COPY cache?

No. The COPY and ADD checksum covers file contents and metadata such as permissions, but not modification or access times. Touching a file or checking it out again with identical contents does not cause a cache miss.

How do I list or check the Docker build cache?

docker buildx du lists build cache records and their size (add --verbose for per-record details such as last use and whether it is shared). docker system df shows the total build cache size. To check which steps of a particular build hit the cache, run the build with --progress=plain and look for CACHED.

How do I force Docker to rebuild without the cache?

docker build --no-cache rebuilds every step. docker build --no-cache-filter <stage> rebuilds only the named stages (BuildKit). docker build --pull re-resolves base images to their latest digest. To rebuild from one line downward, place an ARG before it and pass a new value with --build-arg. None of these empty RUN --mount=type=cache directories.

How do I clear the Docker build cache?

docker builder prune removes dangling build cache; docker builder prune -a removes all unused build cache, not just dangling records; docker builder prune --filter until=72h removes records not used in the last 72 hours. For a non-default buildx builder use docker buildx prune with --builder <name>.

Notes and Limitations

  • The cache is local to the builder by default. CI runners and other machines don't share it unless you export and import it with --cache-to/--cache-from.
  • Build cache consumes disk space and is garbage-collected by the builder. Evicted records turn into misses on the next build.
  • Wildcard sources (COPY *.json ./) are checksummed over the files that match, so adding a new matching file is a miss.
  • The legacy builder (DOCKER_BUILDKIT=0) applies the same key rules but builds strictly line by line, and doesn't support --no-cache-filter, cache mounts, or external cache backends.