Intuition What this page is
The parent note gave you the rules: layers cache, dependencies go early, EXPOSE is only a note-to-self. Rules are cheap. This page runs the machine on every kind of input it can meet — a first-time build, a code-only edit, a dependency change, a base-image bump, the zero case (nothing changed), the degenerate case (everything changed), a real deployment word problem, and a nasty exam twist. After this, no build outcome should surprise you.
Before any symbol appears we define it. We reuse exactly one formula from the parent, restated here so this page stands alone.
Throughout we use one concrete layer stack — the canonical ML Dockerfile from the parent — with made-up but realistic per-layer times so we can compute real numbers:
Now look at Figure 1 (its title reads "Fig 1: Layer stack - find k, sum costs from k upward" ). It draws this exact stack bottom-to-top, marks each layer's cost in yellow, and — with the red dashed bracket — shows the rebuild region for a code edit: only the top three layers (from k = 5 up) get rebuilt, while the blue-outlined layers below stay cached at cost 0 . Every example below is just this picture with a different bracket position.
Every build/run situation this topic can throw falls into one of these cells. The examples below hit each cell exactly.
Cell
Trigger
What changes
Expected k
Covered by
A. Zero-change
rebuild, edited nothing
nothing
none (C = 0 )
Ex 1
B. Code-only edit
edit app.py
copy-code layer L 5 up
k = 5
Ex 2
C. Dependency change
edit requirements.txt
mid-stack
k = 3
Ex 3
D. Base-image bump
change FROM tag
everything
k = 1 (degenerate)
Ex 4
E. Wrong ordering
COPY . . too early
code edit busts pip
k = 3
Ex 5
F. Port mapping
-p host:container arithmetic
runtime, not build
—
Ex 6
G. Ephemeral vs volume
file written at runtime
data survival
—
Ex 7
H. Image-size / slim
full vs slim base
disk + pull cost
—
Ex 8
I. Word problem
CI fleet, pull savings
shared layers
—
Ex 9
J. Exam twist
CMD override + ENTRYPOINT
which args win
—
Ex 10
Worked example Ex 1 — Cell A: the zero-change rebuild
Statement. You run docker build -t myapp:1.0 .. It finishes in 133 s. You immediately run the exact same command again , touching no file. How long does the second build take, and what does Docker print for each layer?
Forecast: guess the seconds before reading on. (Zero? A few? All 133 again?)
Find the first changed layer k . Why this step? k is the only thing the cost formula needs. Nothing on disk changed, so no instruction's inputs differ. There is no first changed layer — k is undefined.
Apply the formula. Why this step? To convert "no k " into a number. With no k , the sum ∑ i = k n has no terms: C rebuild = 0 s.
Predict the log. Why this step? So you recognise a fully-cached build when you see one. Every line reads ---> Using cache.
Verify: Sanity check — a build that reuses all 7 layers must be free of work, and ∑ i = 1 7 c i ⋅ 0 = 0 . Units: seconds. ✓ Answer 0 s .
Worked example Ex 2 — Cell B: edit one line of
app.py
Statement. You change a print in app.py and rebuild. Which layers rebuild, and what's the cost?
Forecast: which layer index does the code first differ at?
Locate the change in the stack. Why this step? k is defined by where in the stack the first altered input lives. app.py enters at layer 5 (COPY . .). Layers 1–4 have identical instructions and inputs → cache hits. This is exactly the red dashed bracket in Figure 1 ("Fig 1: Layer stack - find k, sum costs from k upward" ).
Set k = 5 . Why this step? It's the first layer whose input (the source tree) changed.
Sum from k to top. Why this step? Formula. C rebuild = c 5 + c 6 + c 7 = 2 + 0 + 0 = 2 s.
Verify: The expensive pip install (c 4 = 120 ) sits below k = 5 , so it is skipped — that's the whole point of putting code last. 2 ≪ 133 . ✓ Answer 2 s .
Worked example Ex 3 — Cell C: add a dependency to
requirements.txt
Statement. You append pandas==2.2 to requirements.txt and rebuild. Cost?
Forecast: does this cost more or less than Ex 2? By how much?
Find where requirements.txt enters. Why this step? To pin k . It's copied at layer 3. So layers 1–2 are cache hits, and k = 3 .
Cascade upward. Why this step? Everything above a changed layer rebuilds because its parent hash changed. Layer 4 (pip install) is a child of the now-changed layer 3, so it re-runs in full. Figure 2 ("Fig 2: Dependency change - hash cascade forces children to rebuild" ) draws this cascade — the changed layer in red, the forced-rebuild children in yellow flowing upward.
Sum. Why this step? Formula. C rebuild = c 3 + c 4 + c 5 + c 6 + c 7 = 1 + 120 + 2 + 0 + 0 = 123 s.
Verify: Changing deps should be near-cold (you genuinely must reinstall). 123 s is close to the 133 s cold build, missing only the base-layer 10 s. Makes sense — the base was still cached. ✓ Answer 123 s .
Worked example Ex 4 — Cell D (degenerate): bump the base image
Statement. Security says move FROM python:3.11-slim → FROM python:3.12-slim. Rebuild cost?
Forecast: this is the worst case — why?
Identify k . Why this step? The FROM line is layer 1. Change it and the very bottom of the stack differs, so k = 1 .
Recognise the degenerate case. Why this step? When k = 1 , the sum covers all layers — no caching possible. This is the "everything changed" corner of the matrix.
Sum from 1. Why this step? Formula. C rebuild = ∑ i = 1 7 c i = 133 s — identical to a cold build.
Verify: k = 1 must reproduce the cold-build number, and it does: 133 = 133 . ✓ Lesson: base bumps are unavoidably expensive; batch them, don't do them casually.
Worked example Ex 5 — Cell E: the wrong-ordering trap (steel-manned)
Statement. A teammate "simplifies" the Dockerfile so the source copy happens before the install. The new reordered stack is defined below (all 7 layers, with their new indices and costs):
i
Layer
Instruction
c i (seconds)
1
base
FROM python:3.11-slim
10
2
workdir
WORKDIR /app
0
3
copy-code
COPY . .
2
4
pip-install
RUN pip install -r requirements.txt
120
5
expose
EXPOSE 8000
0
6
cmd
CMD [...]
0
7
(none)
—
0
Note the only structural change from the reference stack: COPY . . moved down to index 3 and pip install sits above it at index 4; copy-reqs is gone because everything is copied at once. They then edit one line of app.py. Cost?
Forecast: compare against Ex 2's clean 2 s.
Use the reordered indices. Why this step? Ordering changed, so the layer indices changed; we must read c i off the new table above, not the reference one. Now COPY . . is index 3 (cost 2) and pip install is index 4 (cost 120).
Find k . Why this step? The app.py edit lands in the copy-all layer, index 3 → k = 3 .
Sum upward. Why this step? pip install is now a child of the changed copy layer, so it rebuilds too: C = c 3 + c 4 + c 5 + c 6 + c 7 = 2 + 120 + 0 + 0 + 0 = 122 s.
Verify: Same one-character edit costs 122 s here vs 2 s in Ex 2 — a 61 × penalty purely from ordering. This is exactly why "deps before code" exists. ✓ Answer 122 s .
Worked example Ex 6 — Cell F: port mapping arithmetic (all cases)
Statement. The app inside the container listens on 8000 (that's the EXPOSEd port). Answer four sub-cases:
(a) docker run -p 8080:8000 myapp — which URL reaches the app?
(b) docker run -p 9000:8000 myapp — which URL now?
(c) docker run myapp (no -p) — can you reach it from your browser?
(d) docker run -p 8000:8080 myapp — does it work?
Forecast: which side of host:container is "your machine"?
State the rule. Why this step? Everything follows from it. -p HOST:CONTAINER — left is the port on your machine , right is the port inside the container the app truly listens on.
(a) Why this step? Map left→right. Browser hits localhost:8080; forwarded to container 8000 where the app lives. ✅ http://localhost:8080 .
(b) Why this step? Only the host side changed. http://localhost:9000 reaches it; 8000 inside is unchanged.
(c) degenerate — no mapping. Why this step? EXPOSE is metadata only; with no -p there is no host port forwarded. Browser cannot reach it. Not reachable. ❌
(d) mismatch. Why this step? Right side says the app is on container port 8080, but the app actually listens on 8000. Docker forwards localhost:8000 → container 8080, where nothing listens → connection refused . ❌
Verify: Rule check — the app is reachable iff the right-hand (container) number equals the port the process binds (8000). Cases (a),(b) have :8000 ✓; (d) has :8080 ✗; (c) has no mapping ✗. Consistent. ✓
Worked example Ex 7 — Cell G: does my runtime file survive?
Statement. Inside a running container your code writes predictions.csv to /app/out/. You then docker rm the container. Answer for two setups:
(a) plain docker run myapp
(b) docker run -v $(pwd)/out:/app/out myapp
Does predictions.csv still exist after removal?
Forecast: where physically does the write land in each case?
Recall the writable layer is ephemeral. Why this step? A container's top read-write layer is created at run and destroyed at rm. Anything written only there dies with the container. Figure 3 ("Fig 3: Ephemeral writable layer vs mounted volume" ) shows this: the ephemeral layer (red) evaporates on rm, while the mounted volume (green) points to host disk that survives.
(a) Why this step? No volume, so /app/out lives in that ephemeral layer. After rm: gone. ❌
(b) Why this step? -v host:container mounts a real host folder over /app/out. Writes go to your machine's disk, outside the container's lifecycle. After rm: still there on the host. ✅
Verify: Sanity — the parent's mistake callout says "mount a volume for data that must survive." (b) mounts, survives; (a) doesn't, dies. Consistent. ✓
Worked example Ex 8 — Cell H: slim vs full image size
Statement. The full python:3.11 base is ~1000 MB; python:3.11-slim is ~130 MB. Your app layers add 50 MB on top of either. Your CI pulls the base fresh every run, at 40 MB/s network. Compute total image size and cold-pull time for each, and the time saved per CI run.
Forecast: guess the pull-time gap before computing.
Total sizes. Why this step? Image size = base + app layers. Full: 1000 + 50 = 1050 MB. Slim: 130 + 50 = 180 MB.
Pull times. Why this step? time = size / bandwidth. Full: 1050/40 = 26.25 s. Slim: 180/40 = 4.5 s.
Saving. Why this step? Difference per run: 26.25 − 4.5 = 21.75 s.
Verify: Units: MB ÷ (MB/s) = s ✓. Slim is 1050/180 = 5.833 … × smaller, so pull is proportionally faster (26.25/4.5 = 5.833 … ). The two ratios match — good. ✓ Answers: 1050 MB / 26.25 s and 180 MB / 4.5 s , saving 21.75 s each run.
Worked example Ex 9 — Cell I: word problem, a CI fleet sharing a base
Statement. A team ships 8 microservice images, all built on the same 130 MB slim base. Each also has 20 MB of unique app layers. A registry stores content-addressed layers, so shared layers are stored once . (a) How much disk does the registry use for all 8? (b) A Kubernetes node that already cached the base then pulls all 8 fresh app layers — how many MB does it transfer, and at 40 MB/s how long?
Forecast: naïvely you'd think 8 × ( 130 + 20 ) . Why is that wrong?
Store the base once. Why this step? Content-addressing means identical layers dedupe. Base counted a single time: 130 MB. Unique layers counted per service: 8 × 20 = 160 MB.
Registry total. Why this step? Sum deduped store: 130 + 160 = 290 MB. (Naïve would be 8 × 150 = 1200 MB — a 910 MB overcount.)
Node transfer. Why this step? Node already has the base cached, so it pulls only the 8 app layers: 160 MB. Time = 160/40 = 4 s.
Verify: Why this step? We must confirm the dedup arithmetic is self-consistent and that we didn't double-count the base. Dedup check — the base appears exactly once (130 MB) and the unique layers total 8 × 20 = 160 MB, so the store is 130 + 160 = 290 MB, which is ≤ 1200 MB (the naïve overcount) as it must be. Transfer check — the node skips the cached base and pulls only 160 MB, giving 160/40 = 4 s (units: MB ÷ MB/s = s ✓). Both consistent — this is why shared bases make CI fleets cheap. ✓ Answers 290 MB , 160 MB / 4 s .
Worked example Ex 10 — Cell J: the exam twist —
CMD vs ENTRYPOINT override
Statement. A Dockerfile ends with:
ENTRYPOINT [ "python" , "predict.py" ]
CMD [ "--model" , "v1" ]
Predict the exact command executed for each run:
(a) docker run img
(b) docker run img --model v2
(c) docker run img --model v2 --gpu
Forecast: which of the two lines is fixed and which is a replaceable default?
State the roles. Why this step? ENTRYPOINT is the fixed executable — always prepended. CMD supplies default arguments that anything you type after the image name replaces wholesale .
(a) — no run-args. Why this step? With nothing typed after the image name, CMD's defaults stand and get appended to the entrypoint. Executed command = python predict.py --model v1 .
(b) — one flag pair supplied. Why this step? You provided args, so they replace the entire CMD (they do not merge per-flag). Result = python predict.py --model v2 . Note v1 is gone entirely, not overridden — the whole default list was discarded.
(c) — extra tokens supplied. Why this step? Same replacement rule; your full token list --model v2 --gpu replaces CMD and is appended to the fixed entrypoint. Result = python predict.py --model v2 --gpu .
Verify: Invariant — ENTRYPOINT (python predict.py) prefixes all three; CMD (--model v1) appears only in (a) where no run-args were given. (b) and (c) drop v1 completely. This is precisely "container behaves like a CLI tool" from the parent — docker run img <args> swaps the arguments without editing the image. ✓ Answers: (a) python predict.py --model v1, (b) python predict.py --model v2, (c) python predict.py --model v2 --gpu.
Recall Cover the matrix from memory
Q: Editing only app.py — which layer index is k , and why isn't pip install rebuilt? ::: k = 5 (the COPY . . layer); pip install is layer 4, below k , so it's a cache hit.
Q: Changing requirements.txt costs almost as much as a cold build — why "almost" and not exactly? ::: k = 3 , so layers 1–2 (base + workdir, cost 10 + 0 ) are still cached; only the base's 10 s is saved.
Q: Which single Dockerfile mistake turns a 2 s rebuild into 122 s? ::: Putting COPY . . before pip install, dropping k below the pip layer so code edits bust it.
Q: -p 8000:8080 when the app listens on 8000 — reachable? ::: No; the container side must be 8000. This forwards to 8080 where nothing listens → refused.
Q: A file written at runtime with no volume — survives docker rm? ::: No; the writable layer is ephemeral. Mount -v host:container to persist.
Q: docker run img --model v2 with CMD ["--model","v1"] — final command? ::: python predict.py --model v2; run-args replace the whole CMD, so v1 vanishes.
Mnemonic The one number that governs everything
"Find k , sum up from k ." Every build cost on this page is just: locate the first changed layer, add its cost and everything above it. Zero-change → no k → free. Base bump → k = 1 → full price.
Editing only source code sets the first-changed layer k to which position in the canonical stack? k = 5, the COPY . . layer; pip install (layer 4) stays cached.
Why does changing requirements.txt cost ~123 s but a base bump costs the full 133 s? requirements enters at k=3 (base still cached, saving 10 s); FROM change makes k=1, nothing cached.
In -p HOST:CONTAINER, which number must equal the port the app actually binds? The right-hand CONTAINER number.
With ENTRYPOINT fixed and CMD as defaults, what happens to CMD when you pass run-time args? The run-time args replace the entire CMD wholesale, not per-flag.