Docker Multi-Stage Build Cache Miss: Fix in 5 Minutes
Multi-stage Docker builds re-running every layer from scratch even when nothing relevant changed? Here is exactly how to diagnose layer ordering, build context, and BuildKit cache export issues causing multi-stage cache misses.
Multi-stage builds add a specific layer-caching failure mode single-stage Dockerfiles don't have — a cache miss in an early stage cascades to every stage that depends on it, even stages that changed nothing themselves. Here is how to find where the cache actually breaks.
Step 1: See Which Layers Are Actually Cache Missing
docker build --progress=plain -t myapp . 2>&1 | grep -E "CACHED|RUN|COPY"
# #8 [build 3/6] RUN npm ci
# #8 CACHED ← this hit cache
# #9 [build 4/6] COPY . .
# #9 0.421s ← this did NOT hit cache — everything after it won't either--progress=plain shows every step's cache status explicitly — find the first non-CACHED line, that's where the chain actually breaks, not necessarily where you assumed.
Cause 1: COPY Instruction Ordering Invalidates Cache Too Early
# BAD — copying everything before installing dependencies means
# ANY file change (even a README edit) invalidates the npm install cache
FROM node:20 AS build
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run buildDocker's layer cache is invalidated the moment any file in a COPY changes — copying the whole repo before the dependency install means every single code change re-runs npm ci from scratch, even though only the lockfile matters for that step.
Fix — copy only what each step actually needs, in dependency order:
FROM node:20 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci # Only invalidated when package-lock.json changes
COPY . .
RUN npm run build # Only this re-runs on code changesCause 2: Multi-Stage COPY --from Breaking the Chain Between Stages
FROM node:20 AS build
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html # This step's cache depends
# on the ENTIRE build stage's outputIf the build stage cache misses for any reason (even a genuinely unrelated dependency bump), the COPY --from=build in the final stage also misses, because it depends on the full output of the previous stage, not just the specific files it copies.
# Confirm this is actually what's happening
docker build --progress=plain -t myapp . 2>&1 | grep -A 2 "COPY --from"This is largely expected behavior, not really fixable by reordering — but you can minimize its blast radius by keeping each stage as narrowly scoped as possible so a cache miss in one doesn't force a rebuild of unrelated work.
Cause 3: Build Context Includes Files That Change on Every Build
# Check what's actually in your build context
docker build --progress=plain -t myapp . 2>&1 | grep "transferring context"
# => transferring context: 245MB ← way too large, likely including node_modules, .git, build artifactsIf .dockerignore is missing or incomplete, files like .git, local node_modules, or build output directories get included in the build context — and if any of THOSE files change between builds (like .git always does), it can invalidate COPY . . layers unnecessarily.
Fix — a proper .dockerignore:
# .dockerignore
.git
node_modules
dist
*.log
.env
coverage
Cause 4: CI Runner Not Persisting BuildKit Cache Between Runs
# BAD — every CI run starts with zero Docker layer cache, defeating the point
- name: Build
run: docker build -t myapp .On ephemeral CI runners (GitHub-hosted runners, fresh VMs), there's no persisted Docker layer cache from the previous run at all — every build is a full cold build regardless of your Dockerfile structure.
Fix — explicitly export/import BuildKit cache using GitHub Actions cache or a registry:
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max# Or cache via a registry, for non-GitHub-Actions CI
docker buildx build \
--cache-from type=registry,ref=myregistry/myapp:buildcache \
--cache-to type=registry,ref=myregistry/myapp:buildcache,mode=max \
-t myapp .mode=max is important — the default mode=min only caches the final image layers, not intermediate build stage layers, which defeats multi-stage caching specifically.
Cause 5: ARG Values Changing Invalidate Everything Downstream
# If BUILD_DATE changes every single build (common mistake), every layer
# after this ARG is declared invalidates on every build, by design
ARG BUILD_DATE
RUN echo "Built at $BUILD_DATE" > /build-info.txt
COPY package.json ./ # This now misses cache too, even though nothing here changed
RUN npm ciFix — move volatile ARGs as late as possible, after the cacheable steps:
COPY package.json package-lock.json ./
RUN npm ci # Cacheable, unaffected by BUILD_DATE now
COPY . .
RUN npm run build
ARG BUILD_DATE # Moved to the end — only affects steps after this point
RUN echo "Built at $BUILD_DATE" > /build-info.txtDiagnostic Checklist
docker build --progress=plain -t myapp . 2>&1 | grep -E "CACHED|RUN|COPY|transferring context"
cat .dockerignore # confirm it excludes volatile/large files
docker buildx build --cache-from ... --cache-to ... # confirm CI is actually persisting cacheMore Docker troubleshooting? Read our Docker build slow no cache fix and Docker Compose network not found fix.
Today I Fixed
Short real fixes from production — posted daily
Stay ahead of the curve
Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.
Related Articles
AWS ECR Push Denied: no basic auth credentials — Fix
Getting 'no basic auth credentials' or 'denied: Your authorization token has expired' when pushing to AWS ECR? Here are the exact commands to fix authentication for Docker, GitHub Actions, and Kubernetes.
Docker Build Fails in CI But Works Locally — Fix
Your Docker build works perfectly on your machine but fails in GitHub Actions, GitLab CI, or Jenkins. Here's every reason this happens and exactly how to fix it.
Docker Build Taking Too Long — Cache and Speed Fixes (2026)
Docker builds taking 10+ minutes every time? Here's how to fix layer caching, use BuildKit properly, and cut build times by 80% with multi-stage builds and cache mounts.