A Docker build reuses a cached layer only while nothing that layer depends on has changed. The moment one layer misses the cache, every layer after it is rebuilt too. This is why COPY . /app placed before RUN pip install turns a 30-second rebuild into a 6-minute one: touching any source file invalidates the copy, and the dependency install downstream of it is thrown away.
Given a Dockerfile as an ordered list of instructions and the set of files that changed in the build context, work out exactly which layers Docker has to rebuild.
Docker's cache rules, simplified to the ones that matter here:
Source-path matching rule for this problem: split the instruction on whitespace. The last operand is the destination; all earlier operands that do not start with -- are sources. A source . (or ./) covers every changed file. Any other source s (after stripping a leading ./ and any trailing /) covers a changed file f when f == s or f starts with s + "/".
Implement:
def rebuilt_layers(instructions, changed_files):
Return a list of the 1-based indices of the instructions that are rebuilt, in ascending order. Return [] when the whole build is a cache hit.
insts = [
"FROM python:3.13-slim",
"WORKDIR /app",
"COPY . /app",
"RUN pip install -r requirements.txt",
'CMD ["python", "app.py"]',
]
print(rebuilt_layers(insts, ["app.py"]))
Output:
[3, 4, 5]
COPY . /app covers app.py, so layer 3 misses; the cascade rule rebuilds 4 and 5 as well — including the expensive pip install.
insts = ["FROM python:3.13-slim", "WORKDIR /app", "COPY . /app", "RUN pip install -r requirements.txt", 'CMD ["python", "app.py"]'] print(rebuilt_layers(insts, ["app.py"]))
[3, 4, 5]
COPY . /app has source ., which covers every changed file, so layer 3 misses the cache. The cascade rule then forces layers 4 and 5 to rebuild, which is why the dependency install re-runs on every source edit. Layers 1 and 2 are untouched and stay cached.
COPY and ADD read from the build contextCOPY --from=<stage> instruction is never invalidated directly by a changed context file