Layer Cache & Build Ordering
How Docker’s layer cache works
Section titled “How Docker’s layer cache works”Every Dockerfile instruction produces a layer. Docker stores each layer in a local cache keyed by the instruction text and all layers beneath it. When you run docker build again, Docker checks each instruction from top to bottom:
- If nothing upstream has changed, it prints
CACHEDand reuses the stored layer — instantly. - The moment any layer changes, Docker invalidates every layer below it and rebuilds from that point on.
This means instruction order has a direct impact on how often you wait for a full rebuild.
The golden rule: order from stable to volatile
Section titled “The golden rule: order from stable to volatile”Place instructions that change rarely near the top of the Dockerfile and instructions that change often near the bottom. Dependencies change far less frequently than application code.
Bad ordering — cache busts on every code change
Section titled “Bad ordering — cache busts on every code change”# syntax=docker/dockerfile:1FROM node:22-alpineWORKDIR /appCOPY . . # ← copies everything including sourceRUN npm ci # ← reinstalls ALL deps on every code changeCMD ["node", "src/index.js"]Every time you edit a .js file, COPY . . changes, which invalidates the RUN npm ci layer. npm ci runs from scratch on every build — even though package.json did not change.
Good ordering — dependencies cached independently
Section titled “Good ordering — dependencies cached independently”# syntax=docker/dockerfile:1FROM node:22-alpineWORKDIR /appCOPY package.json package-lock.json ./ # ← only manifestsRUN npm ci # ← cached unless manifests changeCOPY src/ ./src/ # ← source copied AFTER installCMD ["node", "src/index.js"]Now npm ci is only re-executed when package.json or package-lock.json actually change. A code-only edit skips straight to COPY src/ and finishes in seconds.
Combine RUN steps to reduce layers
Section titled “Combine RUN steps to reduce layers”Each RUN instruction is a layer. Splitting related commands across multiple RUN lines means more layers and more intermediate filesystems to store.
# Inefficient — three layers for one logical operationRUN apt-get updateRUN apt-get install -y curl gitRUN rm -rf /var/lib/apt/lists/*# Efficient — one layer, one cache entryRUN apt-get update \ && apt-get install -y curl git \ && rm -rf /var/lib/apt/lists/*The combined form also ensures rm -rf /var/lib/apt/lists/* runs in the same layer as the install, actually removing the cache bytes from the image.
Pin package versions
Section titled “Pin package versions”Unpinned versions break build reproducibility and can cause unexpected cache busts when a new version is published.
# Unpinned — different installs on different daysRUN apt-get install -y curl
# Pinned — reproducible and explicitRUN apt-get install -y curl=8.5.0-2For npm, package-lock.json (used by npm ci) already pins all transitive versions — always commit it.
BuildKit mount cache (advanced)
Section titled “BuildKit mount cache (advanced)”BuildKit supports --mount=type=cache to share a persistent cache directory across builds without baking it into a layer. This is especially effective for package managers.
# syntax=docker/dockerfile:1FROM node:22-alpineWORKDIR /appCOPY package.json package-lock.json ./RUN --mount=type=cache,target=/root/.npm \ npm ciCOPY src/ ./src/CMD ["node", "src/index.js"]The npm cache at /root/.npm is preserved between builds in a separate BuildKit cache, not inside the image layer. Subsequent installs resolve packages from the local cache rather than the network — significantly faster on slow connections.
Hands-on: cache comparison
Section titled “Hands-on: cache comparison”The snippet below builds a simple image twice. The first build downloads the dependency; the second build hits the cache. Watch the timing difference.
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
# GOOD ordering: manifests before source
COPY package.json ./
RUN echo '{}' > package-lock.json && npm install --prefer-offline || true
# Source comes last — cache survives source-only edits
COPY . .
CMD ["node","-e","console.log('Cache-friendly build complete!')"]
# --- First build (cold) ---
# docker build -t myapp:cache .
#
# --- Edit a source file, then rebuild (warm cache) ---
# echo "// change" >> index.js
# docker build -t myapp:cache .
# Notice: "npm install" step shows CACHED on the second run