MLOpsThe MLOps lifecycle · MLflow 3.17 · DVC 3.67 · FastAPI · Docker · GitHub Actions · Evidently · Python 3.11+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
50 small wins to finish your pathNext lesson →

Smaller images with layer caching

Layer caching is Docker's rule that every Dockerfile instruction becomes a layer it can keep: a rebuild runs an instruction again only when that instruction or something above it changed, so the order of the lines decides how long every later build takes.

Last updated: 19 Oct, 2026 · MLOps

The Dockerising the service lesson built the image once and the install of pandas and scikit-learn was most of the wait. You will build it again on nearly every change, and the Docker basics video gives the reason it need not cost the same each time: an image is not one block but many layers stacked on each other, each one the result of a step.

The container image drawn as a stack of its eight build steps, top to bottom. Step 1 of 8, FROM python:3.12-slim, the base image pulled once. Step 2, WORKDIR /app, where everything lands. Step 3, COPY requirements.txt, the pin list and only that. Step 4, RUN pip install with no cache from requirements.txt, the slow step that installs every package. Step 5, COPY src into the image, the package the pipeline lives in. Step 6, COPY config, the stage config. Step 7, COPY params.yaml, schema.yaml and app.py, the files you edit most. Step 8, COPY artifacts, the preprocessor and the model. Step 1 is marked in purple as the base, steps 2 to 6 in green as CACHED, the word the build log prints for a reused layer, and the last two in orange as running again. Below, the reading: after editing app.py the five layers above the edit are reused and only steps 7 and 8 run again, the pip install is one of the reused five, which is the whole reason requirements.txt is copied before the source, and turning the order round stops that being true, because a one-line code edit then reinstalls every package.

Weighing the layers

Before changing anything, it helps to see where the size went. docker history lists an image's layers newest first with what each one added, and the first eight entries are the eight steps of this Dockerfile.

Example
# what each of our own eight steps added to the image, newest first
docker history student-performance:v1 --no-trunc --format '{{.Size}}  {{.CreatedBy}}' \
  | head -8 | sed 's/ # buildkit$//'

The install dwarfs everything else: the project's own files, model included, add single-digit megabytes, and the packages add hundreds. That is the layer worth protecting from a rebuild, and the one a careless Dockerfile throws away on every commit.

Reading the cache off a rebuild

A code change is the everyday case. Editing app.py and building again prints CACHED against every layer Docker reused, and the listing below pulls out the steps those words belong to.

Example
# one line changes in the service, which is what happens most days
echo "# a note for the next reader" >> app.py

# rebuild, and list the steps Docker reused instead of running again
docker build -t student-performance:v1 . 2>&1 \
  | grep -B1 CACHED | sed -n 's/^#[0-9]* \(\[\)/\1/p' | sort

Which layers survived the edit

  • Five of the eight steps were reused, every one of them above the line that changed.
  • The pip install is one of them, so the packages were not touched and the rebuild took a second rather than minutes.
  • Step 7 ran again because app.py is one of the files it copies, and its content is different now.
  • Step 8 ran again too, although artifacts/ did not change: a layer sits on top of the one before it, so once step 7 is new, everything after it is rebuilt as well.
  • Changing a file's timestamp alone does nothing. The cache key for a COPY is the content, so touching a file and rebuilding reuses every layer.

Why requirements.txt is copied before the source

Put together, those two facts decide the order of the file. The pin list changes rarely and the source changes constantly, so the slow step belongs above the fast-changing one. Written the other way round the cache is useless.

dockerfile
# the wrong order: any edit to any file invalidates the install under it
COPY . .
RUN pip install --no-cache-dir -r requirements.txt

With COPY . . first, the layer under the install carries every file in the project. Change one character of app.py and that layer is new, so the install below it is new, so every package is downloaded and built again. The same edit against the right order costs two cheap copies.

Trimming what the image carries

Caching makes rebuilds fast; a few other habits keep the result small. Each one removes something the running service never reads.

  • A slim base image. python:3.12-slim is a stripped Debian with Python on it, rather than a full distribution with compilers and documentation.
  • pip install --no-cache-dir, so pip's download cache is not baked into the layer it was created in. Nothing in the container will ever reinstall from it.
  • A .dockerignore, so the caches, logs, virtual environment and git history named in .dockerignore are not sent to the build at all, which also makes the build itself quicker.
  • Copying folders by name rather than COPY . ., so a stray notebook or dataset in the project root never reaches the image.

The right order against the wrong one

After a one-line code editrequirements firstCOPY . . first
Steps reusedThe five above the editOnly the base and the workdir
pip installReusedRuns again, every package
What a rebuild costsTwo file copiesA full dependency install
Effect on a build serverA short job on every pushThe same long job every push
Effect on the imageNone; same contentsNone; same contents

Where the order pays you back

  • A build server that rebuilds on every push, which the automation part sets up, where the cached install is the difference between a one-minute job and a ten-minute one.
  • A tight edit loop, where you change a route, rebuild and call the container several times an hour.
  • A slow or metered connection, where re-downloading the packages on every build is the real cost.
Watch out. A fresh machine has no cache at all, so the first build there pays the full install whatever the order. The order is what makes the second build onwards cheap. A build server that throws its disk away after every job starts fresh each time and gets no benefit from it, which is one reason a long-lived machine or an explicit build cache is worth setting up.
Try it yourself
  • Edit requirements.txt instead of app.py, rebuild, and count how many steps are cached now.
  • Move COPY src/ ./src/ above the pip install, edit a file in src/, and watch the install run again.
  • Run docker history after a rebuild and find the layer your edit created.

You understood something today that you didn't yesterday.