Build Cache — Why Some Builds Take 45 Seconds and Some Take 8 Minutes
What Is Build Cache in Simple Terms?
Every time Docker builds an image it checks: did this instruction change since last time? If not, it reuses the result from the last build instead of running the instruction again. This is the build cache.
The cache is why your second docker build is much faster than the first. But it is also why moving one COPY instruction in your Dockerfile can turn a 45-second build into an 8-minute build.
First build: Second build (nothing changed):Layer 1: FROM 3s Layer 1: FROM -> cache HIT 0.1sLayer 2: COPY 1s Layer 2: COPY -> cache HIT 0.1sLayer 3: RUN 240s Layer 3: RUN -> cache HIT 0.1sLayer 4: COPY 2s Layer 4: COPY -> cache HIT 0.1sTotal: 246s Total: 0.4s Second build (source code changed):Layer 1: FROM -> cache HIT 0.1sLayer 2: COPY package.json -> cache HIT 0.1sLayer 3: RUN npm install -> cache HIT 0.1s <- still cached!Layer 4: COPY src/ -> cache MISS 2s <- source changedTotal: 2.3s (not 246s because npm install is still cached)Cache Invalidation Rules
Rule 1: If the instruction text changes, cache is busted "RUN npm install" -> "RUN npm install --verbose" = cache bust Rule 2: For COPY/ADD, if any copied file changes, cache is busted COPY package.json ./ -> package.json changed = cache bust COPY . . -> ANY file changed = cache bust Rule 3: Once a layer misses, ALL layers below it also miss Layer 3 busts -> Layers 4, 5, 6 rebuild regardlessCorrect Layer Ordering for Maximum Cache Reuse
# BAD — npm install cache busted on every source changeFROM node:20-alpineWORKDIR /appCOPY . . # copies everything including sourceRUN npm install # cache busted whenever ANY file changesRUN npm run buildCMD ["node", "dist/server.js"] # GOOD — npm install cached unless package.json changesFROM node:20-alpineWORKDIR /appCOPY package.json package-lock.json ./ # only dependency manifestsRUN npm install # cached until package.json changesCOPY . . # source comes AFTER installRUN npm run buildCMD ["node", "dist/server.js"] # Rule: copy things that change LESS OFTEN first# copy things that change MOST OFTEN lastInspecting Cache During Builds
# See exactly which layers hit or miss cachedocker build --progress=plain . # Output:# step 3/8 : COPY package.json package-lock.json ./# ---> Using cache <- HIT# step 4/8 : RUN npm install# ---> Using cache <- HIT (5min saved)# step 5/8 : COPY . .# ---> a84f9c2b1d3e <- MISS (source changed)# step 6/8 : RUN npm run build# ---> Running in b72c8a9f4e1d <- MISS (cascade) # Force a full rebuild ignoring all cachedocker build --no-cache . # Useful when:# - apt packages need updating# - debugging cache-related issues# - verifying build works from scratchBuildKit Cache Mounts
BuildKit cache mounts keep package manager caches between builds without storing them in image layers:
# syntax=docker/dockerfile:1 # npm cache reused between builds, NOT in final imageRUN --mount=type=cache,target=/root/.npm \ npm ci --omit=dev # pip cache reused between buildsRUN --mount=type=cache,target=/root/.cache/pip \ pip install -r requirements.txt # apt cache reused between buildsRUN --mount=type=cache,target=/var/cache/apt \ apt-get update && apt-get install -y curlRegistry Cache for CI/CD
# GitHub Actions — cache layers in registry between CI runs* name: Build with registry cache uses: docker/build-push-action@v5 with: cache-from: type=gha # read from GitHub Actions cache cache-to: type=gha,mode=max # write new layers to cachePLACEMENT PRO TIP**Tip:** Run `docker build --progress=plain .` on your next build to see every layer and whether it hit or missed cache. This single command tells you exactly which instruction is causing slow builds and which part of your Dockerfile ordering needs fixing.
COMMON MISTAKE / WARNING**Common Mistake:** Putting `COPY . .` before `RUN npm install`. This means every single source file change — even a comment in a README — invalidates the npm install cache. Move package.json copy before the install, source code copy after.