Quick fix
# One-liner: move dependency install before COPY (npm example: COPY the manifest first, RUN install, then COPY everything)
# In the Dockerfile: COPY package.json package-lock.json ./ && RUN npm ci before COPY . /app
pip same: COPY requirements.txt ./ -> RUN pip install -r requirements.txt; go: COPY go.mod go.sum ./ -> RUN go mod download.
What you will run into
You change only one business file, yet the whole npm install / pip install layer re-runs every time:
Step 3/4 : COPY . /app
Step 4/4 : RUN npm install # executes on every build, minutes of reinstall
Builds get slower and slower, and it's worse on CI (full reinstall every time).
Why this happens
Docker build cache matching is "parent chain + instruction + inputs":
- Each layer's cache key contains the parent layer ID, the instruction, and that instruction's inputs (checksums for COPY/ADD, the command string for RUN);
COPY . .counts the whole build context as input - changing one file = the COPY layer checksum changes = the cache for that layer and every layer after it is invalidated;RUN npm install's command string didn't change, but its parent layer (the COPY layer) did, so it can't hit the cache either;- Stuff that shouldn't be in the build context (node_modules, .git, cache directories) makes the COPY layer invalidate constantly.
Essentially: the dependency-install layer gets dragged down by business-file changes that shouldn't affect it. The fix direction = make the COPY layer contain only what the install layer really needs (the dependency manifest), and put the install layer first.
The invalidation chain is "input change → layer checksum change → parent chain breaks": Docker records only the parent layer ID, the instruction, and an input digest per layer.
COPY . . takes the whole build context as input, so touching any business file changes the COPY digest;
RUN npm install's cache key includes its parent layer ID, so it loses its cache too — the cache is not "same command, same reuse" but "verify the whole parent chain layer by layer".
Three deterministic takeaways: ① put dependency installs before COPY of business code so they only depend on the lockfile digest; ② tighten the context with .dockerignore to stop unrelated files from jittering the digest;
③ when debugging, find the first line where ---> Using cache disappears — that is the break point, and the changed input is somewhere before it.
Step by step
1. Move dependency install before COPY — Required
COPY package.json package-lock.json ./
RUN npm ci
COPY . /app
Only dependency-manifest changes re-run the install layer; business code changes no longer trigger it.
2. Tighten the context with .dockerignore — Required
Exclude node_modules/.git/build artifacts/cache directories; otherwise COPY . .'s checksum jitters with unrelated files (also shrinks the upload size).
3. Watch the cache markers while debugging — Required
Wherever ---> Using cache appears in the build output is a hit layer; after changing a file, if the COPY layer shows no Using cache, every layer after it re-runs - walk the output back to the first invalidation point.
4. Cache shared across layers (BuildKit) — Required
RUN --mount=type=cache,target=/root/.npm npm ci
Mount the package-manager cache as a standalone cache volume for higher network-layer hit rates.
5. Use --cache-from in CI — Required
Push a remote image as the cache source (buildx --cache-from), working best with the "install-first" structure; with the order reversed, --cache-from can't save the frequently-invalidated install layer either.
How to confirm it works
After changing a business file, the install layer no longer re-runs (measured output shape):
Change app.cfg again and rebuild -> Step 3/4 RUN -- Using cache (install layer doesn't re-run, fix effective)
Before the fix: after changing a file, neither COPY nor RUN shows Using cache (symptom reproduced).
Other environments
| Environment | Notes |
|---|---|
| npm | COPY package.json package-lock.json ./ -> RUN npm ci |
| pip | COPY requirements.txt ./ -> RUN pip install -r requirements.txt |
| go | COPY go.mod go.sum ./ -> RUN go mod download |
| Build context | .dockerignore excludes node_modules/.git/cache |
| BuildKit | --mount=type=cache standalone cache volume |
| CI | --cache-from + install-first structure |
Symptom
Build cache "always invalidates" when building an image:
# Only one business file changed, yet the whole npm install / pip install layer re-runs every time
Step 3/4 : COPY . /app
Step 4/4 : RUN npm install # executes on every build, minutes of reinstall
Signs: the Dockerfile structure has COPY . . before RUN npm install (or pip/go mod); changing any source file changes the COPY layer and the whole dependency-install layer after it re-runs; builds get slower, worse on CI (full reinstall every time).
Cause
Docker build cache matching is "parent chain + instruction + inputs":
- Each layer's cache key contains the parent layer ID, the instruction, and that instruction's inputs (checksums for COPY/ADD, the command string for RUN);
COPY . .counts the whole build context (including any changed business file) as input - changing one file = the COPY layer checksum changes = the cache for that layer and every layer after it is invalidated;RUN npm install's command string didn't change, but its parent layer (the COPY layer) did, so it can't hit the cache either;- Likewise, stuff that shouldn't be in the build context (node_modules, .git, cache directories) makes the COPY layer invalidate frequently.
Essentially: the dependency-install layer gets dragged down by business-file changes that shouldn't affect it. The fix direction = make the COPY layer contain only what the install layer really needs (the dependency manifest), and put the install layer first.
Solution
- Move dependency install before COPY:
COPY package.json /app/+RUN npm installfirst, thenCOPY . /app- only package.json changes re-run the install layer; business code changes no longer trigger it.
- npm:
COPY package.json package-lock.json ./->RUN npm ci; - pip:
COPY requirements.txt ./->RUN pip install -r requirements.txt; - go:
COPY go.mod go.sum ./->RUN go mod download.
- Tighten the context with .dockerignore: exclude node_modules/.git/build artifacts/cache directories; otherwise
COPY . .'s checksum jitters with unrelated files (also shrinks the upload size). - Watch the cache markers while debugging: wherever
---> Using cacheappears in the build output is a hit layer; after changing a file, if the COPY layer shows noUsing cache, every layer after it re-runs - walk the output back to the first invalidation point. - Cache shared across layers (BuildKit):
RUN --mount=type=cache,target=/root/.npmetc., mounting the package-manager cache as a standalone cache volume for higher network-layer hit rates. - Use --cache-from in CI: push a remote image as the cache source (buildx
--cache-from), working best with the "install-first" structure; with the order reversed, --cache-from can't save the frequently-invalidated install layer either.
Verification Evidence
- Local measurement (2026-08-29, WSL Ubuntu, Docker 29.1.3 legacy builder, alpine:3.20 fully local no network):
- Problem structure:
COPY . /app+RUN echo install-deps > /installed.txt(RUN command string unchanged throughout); - 1. First build: all four steps execute;
- 2. Rebuild after changing app.cfg:
Step 2/4 WORKDIR -- Using cache, butCOPY . /appandRUNboth show noUsing cache- the install layer gets dragged into re-running by a business-file change (symptom reproduced); - 3. Fixed structure:
RUN echo install-deps > /installed.txtfirst +COPY app.cfg /app/app.cfgafter, first rebuild RUN executes once (layer chain changed); - 4. Change app.cfg again and rebuild:
Step 3/4 RUN -- Using cache, install layer no longer re-runs (fix effective); - Cleanup: test image deleted.
- Source comparison: SO #43073137 (COPY doesn't detect updates/cache not working) + farscapian/lnplay#35 (COPY of all source above RUN yarn invalidates the install layer) - same kind.
Evidence details kept locally (2026-08-29)
Related canonicals
Read in order: precondition -> same root / same domain / related:
- Precondition (rule out first):
- Cannot connect to the Docker daemon (precondition: build depends on the daemon)
Applicable tools (1)
This problem is linked from the tool pages below — reachable from either side.
dockerBundled solutions (combos)
This problem is part of the following verified bundles.
Source & evidence
Source: https://stackoverflow.com/questions/43073137/why-is-copy-in-docker-build-not-detecting-updates
← Coding Agents & Terminal domain | Problem Catalog Experience | Facing another issue? Go to the Helpdesk