Layer caching and .dockerignore
Each instruction is a cached layer
Every instruction in a Dockerfile produces a layer —
visible directly in the build log from earlier in this lesson,
one Step N/6 per instruction. Docker caches each layer, and re-uses it
on the next build if that instruction, and everything before it, hasn’t
changed — skipping the actual work entirely when the cache applies.
The naive order — show the pain
COPY . . copies every file in the project, including application
code that changes constantly. Docker’s cache invalidation cascades
forward — if COPY . . sees any file has changed, that layer’s cache is
invalidated, and so is every layer after it, including RUN pip install. Three builds, in order, tell the whole story — notice the
RUN step’s own timing in each:
FROM python:3.12-slim WORKDIR /app COPY . . RUN pip install -r requirements.txt CMD ["python3", "app.py"]
click "Build" to see the build log
The first build actually installs everything — that’s real work, and the
RUN step is the slow one. Rebuild again with nothing changed at all:
FROM python:3.12-slim WORKDIR /app COPY . . RUN pip install -r requirements.txt CMD ["python3", "app.py"]
click "Rebuild (nothing changed)" to see the build log
Every step, including the install, comes back from cache — fast,
because nothing Docker checked actually changed. Now change
requirements.txt itself (a real dependency change, not just an
unrelated code edit) and rebuild once more:
FROM python:3.12-slim WORKDIR /app COPY . . RUN pip install -r requirements.txt CMD ["python3", "app.py"]
click "Rebuild (after changing requirements.txt)" to see the build log
The slow install comes back the moment something the install step
actually depends on changes. The naive ordering’s real problem isn’t
that caching fails here — it’s that COPY . . sitting before RUN pip install means any file changing, including ones with nothing to do
with dependencies at all, has this same cache-busting effect on the
install layer.
The fix: copy dependencies first, separately
Copying just requirements.txt first, installing from it, and only
then copying the rest of the project means the install layer’s cache
only breaks when requirements.txt itself actually changes:
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python3", "app.py"]
click "Build" to see the build log
Now rebuild after changing a line in app.py — unrelated to
requirements.txt entirely:
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python3", "app.py"]
click "Rebuild (app.py changed)" to see the build log
The install step is skipped entirely — unlike the naive ordering, an
unrelated code change no longer touches the install layer’s cache at
all, because COPY . . now sits after it instead of before.
.dockerignore — the other half of the caching problem
COPY . . doesn’t just risk invalidating the cache — it copies
everything in your project directory into the build context, whether
you actually want it in the image or not: .git/ (your entire version
history), __pycache__/, a local .venv/, and — worth connecting
directly back to
the secrets warning from earlier in this lesson
— potentially a .env file sitting right there in the project root.
.dockerignore works exactly like .gitignore — a plain list of
patterns to exclude, this time from what gets sent into the build
context at all, before any COPY instruction even runs:
.git
__pycache__/
*.pyc
.venv/
.envWith this file present, COPY . . never even sees .env or .git —
they’re excluded before the build context is assembled, not filtered
out afterward. This closes the specific gap from earlier: a
.dockerignore entry for .env means there’s no COPY instruction to
accidentally sweep it in, in the first place, regardless of how the rest
of the Dockerfile is written.
It also has a real caching benefit of its own: __pycache__/ and other
frequently-changing, irrelevant files no longer count as “the project
changed” from Docker’s perspective, since they’re never part of the
build context to begin with — one less thing that can spuriously
invalidate a layer’s cache.
What does Docker's layer cache let a rebuild skip?
Starting from this naive Dockerfile (COPY . . before RUN pip install), with a .git/ directory, a __pycache__/ directory, and a .env file sitting in the project: reorder it so the install layer's cache survives an unrelated code change, and add a .dockerignore excluding .git, __pycache__/, *.pyc, and .env. This is built for real, twice, against an actual Docker daemon — an unrelated line gets appended to app.py between the two builds automatically.