4 · Layers, caching & multi-stage builds
A Docker build that takes 5 minutes can take 5 seconds if you understand layers. Order your Dockerfile so editing code doesn't re-install dependencies, ignore junk with `.dockerignore`, and ship slim images with multi-stage builds.
Each Dockerfile instruction becomes a CACHED layer; Docker reuses a layer until one changes, then rebuilds it and everything below. So COPY your dependencies and install them BEFORE copying your fast-changing code — that way a code edit reuses the expensive `pip install` layer instead of redoing it.
Without this:
Get the layer order wrong and every one-line code change triggers a full `pip install` of scikit-learn, NumPy, and friends — minutes of wasted build time on every commit, and bloated images shipping your whole build toolchain.
A Docker image is built up as a stack of layers — one per instruction in the Dockerfile. Each FROM, COPY, RUN produces a new layer on top of the previous. The crucial feature: Docker caches layers. On a rebuild, it walks down the Dockerfile and reuses a cached layer as long as that instruction and its inputs are unchanged. The moment one layer's inputs change, that layer and every layer below it are rebuilt from scratch — the cache is "busted" from that point down.
This single fact dictates how to order a Dockerfile. The expensive step is RUN pip install -r requirements.txt (downloading and compiling NumPy, scikit-learn, etc. — often minutes). Your code, by contrast, changes constantly. So you must put the slow, rarely-changing step above the fast, often-changing step:
COPY requirements.txt . ← changes rarely
RUN pip install -r requirements.txt ← expensive, but cached when reqs unchanged
COPY . . ← changes on every commit
Now a one-line edit to app.py only busts the final COPY . . layer — the pip install layer above it is reused from cache, and the rebuild takes seconds. If you'd written COPY . . before pip install, every code edit would change the copy layer and force a full reinstall. That ordering is the single most important Docker performance habit.
.dockerignore. COPY . . copies the entire build context. You don't want .git, __pycache__, virtualenvs, datasets, or notebooks in your image — they bloat it and, worse, changing any of them busts the copy-layer cache. A .dockerignore file (same idea as .gitignore) excludes them from the build context entirely.
Multi-stage builds. Sometimes building your dependencies needs tools (compilers, build headers) that the running app doesn't. A multi-stage Dockerfile uses a fat "builder" stage to compile/install everything, then copies only the finished artifacts into a clean, slim "runtime" stage. The build tools stay behind in the builder and never ship. The final image is much smaller — faster to push, pull, and deploy, and a smaller attack surface.
A multi-stage Dockerfile: a fat `builder` stage installs dependencies, then only the finished packages are copied into a slim runtime stage — the build toolchain never ships.
A `.dockerignore` excluding `.git`, caches, virtualenvs, notebooks, datasets, and artifacts from the build context — smaller images and a stable copy-layer cache.
In a Dockerfile, why copy `requirements.txt` and run `pip install` BEFORE `COPY . .` (the rest of the code)?
- Each Dockerfile instruction is a cached layer; when one layer's input changes, that layer and every layer below it rebuild from scratch.
- Copy + install dependencies before copying code, so code edits reuse the expensive `pip install` layer; use `.dockerignore` to keep junk out of the build context.
- Multi-stage builds install in a fat builder stage and copy only the finished packages into a slim runtime stage — the build toolchain never ships, shrinking the final image.
ML images are heavy (NumPy, scikit-learn, sometimes CUDA), so layer caching and multi-stage slimming are what keep CI builds and cloud deploys fast enough to iterate on a model service.
If you remove it: Every commit would trigger a full multi-minute dependency reinstall and ship a bloated image — turning the fast iteration loop this track is built around into a slow, painful one.