The physics of Docker build caching

piobio2 pts0 comments

The physics of Docker build caching | Blacksmith

Blacksmith Sandboxes Coming soon

MENU<br>Close

SupportLog InLOG IN

Jul 31, 2026

The physics of Docker build caching<br>Concepts, rules of thumb, and what dissecting a few hundred CI builds says about them.

Piotr Bejda<br>Member of Technical Staff

TL;DR<br>Concepts, rules of thumb, and what dissecting a few hundred CI builds says about them.

Get started!<br>Try us Free

Advice about Docker build caching tends to arrive as folklore: put the COPY late, enable some cache action, hope. The trouble is that "Docker caching" is not one cache: it's at least three, with different lifetimes, different invalidation rules, and different failure modes. Which of them matters, and whether the folklore helps, does nothing, or actively hurts, depends on the shape of the build in front of you.<br>There are too many build shapes to cover one by one: every stack, monorepo layout, and orchestration has its own wrinkle. So this post aims for a different kind of completeness: the concepts and rules of thumb that decide the outcome in any scenario, each illustrated with a measurement from a benchmark lab we built. The lab spans 18 scenarios across Go, Python, Rust, Node, Java, and Bazel, run on real CI runners, comparing every commonly used caching backend. Where our own product wins, I'll show it; where something is genuinely missing, ours included, I'll say so.<br>The mental model: three caches, one invalidation wave<br>The concepts here stand on their own, so no Dockerfile expertise is required to follow along. When the examples reference specific instructions (FROM, COPY, RUN, multi-stage builds), Docker's Dockerfile overview and building best practices are good prose introductions to lean on.<br>A Docker build is a chain of instructions, each producing a layer keyed by the instruction and the content that feeds it. A change invalidates its layer and every layer after it , a wave that propagates to the end of the chain. That global reach is what makes the wave worth optimizing for: how far it travels decides how much of the build re-runs, on every single change.<br>(Strictly, the build graph is a DAG: with multi-stage builds each stage is its own chain, joined by COPY --from edges, and the wave only reaches the stages that depend on the change. But within a stage it's always a chain, most Dockerfiles are effectively a single chain, and that's what the examples here show; everything said about the wave applies per-chain.)<br>The chain is only the first cache. Two more sit beside it:<br>Mount caches (RUN --mount=type=cache): directories that persist across builds. They don't stop the wave, they cushion it: when the wave re-runs an instruction, the instruction finds its mount still warm, so a re-run isn't a from-scratch run. This is where package-manager and compiler state lives (GOCACHE, cargo's registry and target/, pip's wheels, pnpm's store).<br>The image store (/var/lib/docker): pulled base images, built images, everything the daemon itself caches. It touches the chain at exactly one point, the FROM layer: when a base image changes, the wave starts at the very top, and whether the new base is a fast local read or a slow registry pull depends on this store. The wave never invalidates anything in it. And it lives well beyond builds: docker run, service containers, and image distribution are all reads against this store.<br>A question that will come back for each of these caches, in every setup below: where does it live, and does it survive to the next CI job? Everything that follows is a zoom-in on some part of this picture.

One more piece of shared vocabulary: the change classes from the diagram above are also how every measurement in this post is structured. Each scenario builds four times, and each build probes one class:<br>cold : the first build, nothing cached anywhere.<br>warm : rebuild with nothing changed; every layer should come from cache.<br>source-change : a code edit, dependency manifests untouched; the wave starts below the install step.<br>dep-change : a manifest/lockfile bump; the install step and everything after it re-runs.<br>Rule of thumb #1: think in change classes, not in builds. Your median CI build is a source-change, not a cold build. A cache setup that looks great on the "second build of the same commit" demo and terrible on a one-line code edit is optimizing the phase you hit least.<br>Order layers by change frequency<br>The general guideline: figure out which of your steps are expensive and stable, and order the Dockerfile so that frequently changing content sits below them. The most common instance is manifest-first ordering: copy your dependency manifest first, install dependencies, and only then copy the source. The expensive install layer then invalidates only when the manifest changes, not on every commit.<br>We measured the same service both ways (COPY . . before the install vs manifest-first) across five stacks. Source-change build times:

stack<br>manifest-first<br>COPY . ....

build wave change docker builds chain

Related Articles