HAMZA KAREEM / CONCEPTS hamzawi.dev

Lesson 21 min read

Docker Layer Caching

why every build reinstalls ALL your dependencies

Developer toolingdeveloper tooling / DevOps

The symptom

You change one line in app.py, rebuild, and Docker spends 4 minutes reinstalling 200 packages... again. Nothing about your dependencies changed. Why?

The Dockerfile doing it:

FROM python:3.12-slim

WORKDIR /app

COPY . .

RUN pip install -r requirements.txt

CMD ["python", "app.py"]

Reasoning through it

Each instruction (FROM, COPY, RUN…) builds a cached layer. On rebuild Docker reuses a layer ONLY if that instruction and everything above it are unchanged. The moment one layer's input changes, that layer AND every layer after it rebuild from scratch.

Look at line 3: COPY . . copies your ENTIRE source tree — including app.py. So editing app.py changes the COPY layer's input → its cache busts → and the next line, RUN pip install, sits downstream of it → so it reruns too. You reinstall everything because you touched one source file.

The fix: order by change frequency

Copy the thing that rarely changes (your dependency manifest) BEFORE the thing that changes every commit (your code):

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt . # changes rarely

RUN pip install -r requirements.txt # cached unless requirements.txt changes

COPY . . # changes every commit

CMD ["python", "app.py"]

Now editing app.py only busts that last COPY. The pip install layer stays cached. Build drops from minutes to seconds.

The rule

Order instructions least-likely-to-change (top) → most-likely-to-change (bottom). Cache invalidation cascades DOWNWARD only — never upward — so put volatile steps last.

Gotchas that trip people up

  • Same pattern for Node: COPY package.json . → RUN npm ci → COPY . ..
  • A layer's cache key is file CONTENTS, not just names — editing requirements.txt rightly busts it.
  • .dockerignore matters: if .git / node_modules aren't ignored, COPY . . changes constantly and you lose caching anyway.
  • "Fast on my laptop, slow in CI" is usually an empty cache — enable BuildKit + --cache-from / registry cache.

Your exercise

Reorder this for caching:

COPY . .
RUN npm ci

What goes first, and which single file should you COPY before npm ci? Bonus: which BuildKit feature (hint: RUN --mount=type=…) lets npm's download cache survive EVEN when package.json changes?