How it works
The explanation, with examples you can run.
How the cache decides
FROMis keyed by the base image digest.COPYandADDare keyed by a checksum of the copied files' contents and metadata. Changing a file that is copied invalidates the step; modification times alone do not.RUNis keyed by the command text only. Docker does not know thatapt-get updatewould return something different today.- When one step misses, every step after it is rebuilt.
An ordering that caches well
FROM node:22-alpineWORKDIR /app# 1. Only the dependency manifests: this layer changes when dependencies changeCOPY package.json package-lock.json ./RUN npm ci --omit=dev# 2. The source code: changes on every commit, so it comes lastCOPY . .USER nodeEXPOSE 3000CMD ["node", "server.js"]After editing server.js, a rebuild reuses everything up to the final COPY:
docker build --progress=plain -t hello:1 .# #6 [3/5] COPY package.json package-lock.json ./# #6 CACHED# #8 [4/5] RUN npm ci --omit=dev# #8 CACHED# #9 [5/5] COPY . .# #9 DONE 0.0sWith COPY . . placed before npm ci, the same one-line edit shows RUN npm ci running again (added 1 package ... in 439ms here, minutes in a real project).
Keep the context small
docker build . sends the directory (the build context) to the builder. Anything in it can be copied, and anything copied can change a cache key. A .dockerignore next to the Dockerfile keeps it lean:
node_modules.git*.log.envDockerfile.dockerignoreWithout it, COPY . . would copy your local node_modules (built for macOS, perhaps) over the ones just installed, any edit under .git would bust the cache, and .env would end up inside the image for anyone who pulls it.
Habits that help
- Combine
apt-get updateandapt-get installin oneRUN, so the package list is never older than the install, and clean the package cache in the same step. - Pin the base image (
node:22-alpine, or better a digest) rather thanlatest, so the first layer does not change under you. - Use the lockfile and the clean-install command (
npm ci,pip install -rwith hashes,go mod download), so the install step is reproducible. - Copy only what a step needs. A tool that only needs
go.modshould not see the whole tree. - In CI, where every runner starts empty, export the cache to the registry or the CI cache (
--cache-to/--cache-from), or every build starts from zero.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.