Coding Agents & Terminal · recorded 2026-08-29 · applies to 1 tool(s) ✓ Verified

Problem: Docker build cache not working (install layers re-run on every code change) - COPY order and .dockerignore —— Trove problem library

Verified 2026-08-29Docker build cache keeps missing because of COPY order — COPY dependency manifests first, RUN install, and COPY the full tree last.

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

EnvironmentNotes
npmCOPY package.json package-lock.json ./ -> RUN npm ci
pipCOPY requirements.txt ./ -> RUN pip install -r requirements.txt
goCOPY 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

  1. Move dependency install before COPY: COPY package.json /app/ + RUN npm install first, then COPY . /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.
  1. 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).
  2. Watch the cache markers while debugging: 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.
  3. Cache shared across layers (BuildKit): RUN --mount=type=cache,target=/root/.npm etc., mounting the package-manager cache as a standalone cache volume for higher network-layer hit rates.
  4. 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, but COPY . /app and RUN both show no Using cache - the install layer gets dragged into re-running by a business-file change (symptom reproduced);
  • 3. Fixed structure: RUN echo install-deps > /installed.txt first + COPY app.cfg /app/app.cfg after, 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:

Applicable tools (1)

This problem is linked from the tool pages below — reachable from either side.

docker

Bundled solutions (combos)

This problem is part of the following verified bundles.

Source & evidence

Recorded Verified ✓ VerifiedApplies to 1 tool(s)
Entries contain locally measured or reviewable-source solutions; unmeasured items are explicitly labeled.

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