Every instruction in a Dockerfile — FROM, COPY, RUN, ENV, and the rest — produces its own layer, and Docker caches each one so a rebuild can skip the instructions that didn't need to change. The mechanic behind that cache is simple once you see it, and it explains both why some rebuilds finish in under a second and why others silently reinstall every dependency on every single build.
How a layer becomes a cache hit
For COPY and ADD, Docker checks the content of the files being copied — a checksum, not a timestamp, so touching a file without changing its contents still counts as unchanged. For RUN, ENV, and most other instructions, Docker compares the exact instruction text against the one used last time. Either way, a cache hit also requires the parent layer — the one built by the previous instruction — to itself have been a cache hit. That last condition is the one that causes almost every real-world caching surprise.
One miss, and everything after it rebuilds too
Docker doesn't re-check each instruction independently. The moment one layer misses the cache, every instruction after it in the file rebuilds as well — not because their own inputs changed, but because their parent layer did. An unchanged RUN line sitting after a changed COPY still reruns from scratch, identical command and all.
The classic mistake: copying everything before installing dependencies
The single most common Dockerfile caching problem is ordering source code ahead of dependency installation:
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "index.js"]COPY . . includes every source file in the project, so its checksum changes on almost every commit — a one-line change to a comment invalidates it exactly as much as a real feature would. Since RUN npm install comes after it, that layer misses the cache too, and the entire dependency tree reinstalls on every build regardless of whether package.json actually changed.
Copying only the dependency manifests first means npm install only reruns when a dependency actually changed — not on every source edit.
The fix is copying only the dependency manifests first, installing, and copying the rest of the source last:
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "index.js"]Now COPY package*.json ./ only changes when a dependency is actually added, removed, or bumped — the far less frequent event — and npm install keeps its cache hit on every build in between.
Multi-stage builds cache per stage, independently
Each stage in a multi-stage Dockerfile has its own layer cache, evaluated separately from every other stage. A stage that only runs tests can miss its cache and rebuild without forcing the build stage before it — or the runtime stage after it — to rebuild anything, as long as neither of those depends on the test stage's output. See How to Write a Multi-Stage Dockerfile for how to structure the stages themselves.
BuildKit cache mounts: caching what the layer cache can't
Reordering COPY instructions only helps the layer cache skip a step entirely — it doesn't help once that step genuinely has to rerun, like after a real dependency change. BuildKit's cache mounts solve that different problem:
RUN --mount=type=cache,target=/root/.npm \
npm installThe mounted directory persists across builds independently of the layer cache, so even when npm install has to rerun, the package manager's own download cache is still there — only new or changed packages get fetched over the network, instead of every package in the tree.
Try it yourself
Dockerfile Linter flags instruction ordering and other Dockerfile issues before you find out about them from a slow rebuild.
FAQ
Does changing a comment in a Dockerfile invalidate the cache?
Only for the instruction the comment is attached to, and only if it changes that instruction's exact text. A standalone # comment line on its own isn't an instruction and produces no layer, so it doesn't affect caching at all.
Does reordering unrelated instructions ever help on its own?
Only if it changes which instructions come before the ones that change often. Moving a rarely-changing instruction earlier — ahead of anything that changes frequently — is exactly what fixes the dependency-install problem above, and the same principle applies to any other slow, infrequently-changing step.
Does --no-cache fix a stale cache problem?
It works, but it discards the cache for every layer, not just the stale one — every instruction in the file reruns, including ones that were caching correctly. Fixing the instruction order (or the file being copied) is the targeted fix; --no-cache is a blunt, slow one.