Layers & the Build Cache
How the build cache works
Section titled “How the build cache works”Every Dockerfile instruction creates a layer. Docker stores these layers on disk and assigns each one a cache key based on:
- The instruction text itself
- The parent layer’s cache key
- (For
COPY/ADD) the checksum of the copied files
When you rebuild an image, Docker checks each instruction against the cache. If the cache key matches, Docker reuses the cached layer — instant, zero-cost. If the cache key does not match (the instruction changed, or a copied file changed), Docker invalidates that layer and every layer after it, rebuilding them from scratch.
This is the critical rule:
Cache invalidation cascades downward. Once a layer is invalidated, all subsequent layers are also rebuilt.
Why instruction order matters
Section titled “Why instruction order matters”Consider this poorly ordered Dockerfile:
# BAD: copies all source code firstFROM node:22-alpineWORKDIR /appCOPY . . # ← copies everything, including app sourceRUN npm ci --omit=dev # ← runs after COPY — cache busted on every code changeEvery time you change a single source file, COPY . . is invalidated, which also invalidates RUN npm ci. That means npm reinstalls all dependencies on every build — even if package.json did not change.
The fix is to copy dependency manifests first, install, then copy source:
# GOOD: dependency install is cached separatelyFROM node:22-alpineWORKDIR /appCOPY package.json package-lock.json ./ # ← only changes when deps changeRUN npm ci --omit=dev # ← cached as long as package.json unchangedCOPY . . # ← source changes only bust this layerNow npm ci is only re-run when package.json or package-lock.json changes. Changing app code only invalidates the final COPY . . and anything below it.
Seeing the cache in action
Section titled “Seeing the cache in action”First build — everything is a fresh download:
[+] Building 14.7s (7/7) FINISHED => [1/5] FROM docker.io/library/node:22-alpine 5.2s => [2/5] WORKDIR /app 0.0s => [3/5] COPY package.json package-lock.json ./ 0.0s => [4/5] RUN npm ci --omit=dev 8.1s => [5/5] COPY . . 0.0s => exporting to image 1.4sSecond build — only app source changed, npm ci is cached:
[+] Building 1.1s (7/7) FINISHED => [1/5] FROM docker.io/library/node:22-alpine 0.0s => CACHED [2/5] WORKDIR /app 0.0s => CACHED [3/5] COPY package.json package-lock.json ./ 0.0s => CACHED [4/5] RUN npm ci --omit=dev 0.0s => [5/5] COPY . . 0.0s => exporting to image 1.1sThe build went from 14.7 s to 1.1 s because four of five layers were served from cache.
General ordering rule
Section titled “General ordering rule”Order instructions from least frequently changing to most frequently changing:
FROM(base image — rarely changes)- System package installs (
RUN apt-get install ...) - Dependency manifests copied + installed (
COPY package.json ./+RUN npm ci) - Application source code (
COPY . .) - Build steps that depend on source (
RUN npm run build)
Hands-on: observe the cache
Section titled “Hands-on: observe the cache”The snippet below builds a tiny image twice. Run it and watch the second build use CACHED layers.
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
# Step 1: copy only the manifest and install
RUN echo '{"name":"demo","version":"1.0.0"}' > package.json
RUN npm install --omit=dev 2>/dev/null || true
# Step 2: copy "source" (simulated with an echo)
RUN echo "console.log('Hello from app v1');" > app.js
CMD ["node", "app.js"]
# --- first build ---
# docker build -t cachetest .
# docker run --rm cachetest
# --- change "source" and rebuild: only the last RUN reruns ---
# docker build -t cachetest .