🐳 Your Docker Build Keeps Using Old Code Because of a Cache Layer You Didn’t Expect
Source code changes, `docker build` runs, and the container still behaves like the OLD code – not because Docker ignored the change, but because a `COPY` instruction earlier in the Dockerfile bundled source code together with something that rarely changes, so Docker’s layer cache sees no difference at that step and happily reuses a stale layer for everything after it.
🔎 The Problem
FROM node:20 WORKDIR /app COPY . . # <- copies EVERYTHING, including source, in one layer RUN npm install CMD ["node", "server.js"] # Docker caches each instruction'"'"'s layer based on its inputs. Because # `COPY . .` bundles package.json AND all your source files into ONE # layer, changing app.js still invalidates that layer correctly here - # but the REAL problem shows up in the opposite, much more common # ordering mistake below.
✅ Fix: Order Instructions From Least-Changing to Most-Changing
- Copy dependency manifests first and install dependencies BEFORE copying the rest of the source: `COPY package.json package-lock.json ./` then `RUN npm install`, and only THEN `COPY . .` – now changing application code invalidates only the final, fast layer, while `npm install` stays cached whenever dependencies haven’t changed.
- This ordering is the single biggest lever for Docker build speed in most projects – going from “reinstall all dependencies on every code change” to “reinstall only when package.json actually changes” is often a 10x-or-more difference in build time.
⚠️ When You Genuinely Need to Bust the Cache
- `docker build –no-cache` forces every layer to rebuild from scratch – useful for confirming a suspected cache issue, but too slow to use as your default build command.
- For CI pipelines specifically, `docker build –build-arg CACHEBUST=$(date +%s)` (with a corresponding unused `ARG CACHEBUST` line right before the step you want to force) invalidates the cache from exactly one point onward, without throwing away the whole cache.
A Docker layer isn’t checked against what you meant to change – it’s checked against what its own instruction’s inputs actually were, and a badly ordered COPY can hide the difference from it entirely.
