Dockerfile Layer Cache Invalidation
Problem Statement
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.
Background
Docker's cache rules, simplified to the ones that matter here:
- A COPY or ADD layer that reads from the build context misses the cache when any of its source paths cover a changed file.
- A COPY --from=<stage> reads from another build stage, not from the build context, so a changed context file never invalidates it directly.
- Every other instruction (FROM, RUN, WORKDIR, ENV, CMD, ...) is identical to last time, so it can only be invalidated by cascade.
- Cascade rule: once a layer misses, all subsequent layers miss as well.
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 + "/".
Your Task
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.
Input Format
- instructions: list of strings, one Dockerfile instruction each, in order.
- changed_files: list of build-context relative paths (strings) that changed.
Output Format
- A list of ints — the 1-based positions of the rebuilt instructions.
Sample
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.
Example
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]
- Inputs:
instructionsis the Dockerfile as five strings, one layer each, numbered 1 to 5 in file order.changed_filesholds a single path,app.py(the only edit). - Layers 1 (the base image) and 2 (the working directory) do not read the build context, so both are cache hits.
- Layer 3 copies from the build context. Its operands are "." and "/app"; the last one is the destination, so the only source is ".", which covers every changed file. Because
app.pychanged, layer 3 misses. - Cascade: if layer misses, every layer misses too. Layer 4 (the dependency install) and layer 5 (the start command) never touch the context, yet both rebuild, giving
[3, 4, 5]. - Notice the cost: the install sits after a whole-context copy, so any edit throws it away. If layer 3 instead copied just a "src" folder,
app.pywould not be covered (a source covers only when or lies inside the folder ), and the answer would be[].
Constraints
- 1 <= len(instructions) <= 60
- 0 <= len(changed_files) <= 50
- Instruction keywords are uppercase; operands are separated by single spaces
- Only
COPYandADDread from the build context - A
COPY --from=<stage>instruction is never invalidated directly by a changed context file - Indices in the returned list are 1-based and ascending