5.3.8 · D4MLOps & Deployment

Exercises — Containerization with Docker

2,489 words11 min readBack to topic

Before we start, one recap of the three words every problem leans on, so no symbol appears un-earned:

We will also reuse one formula from the parent. Here is what every symbol means, spelled out:

Figure — Containerization with Docker

Level 1 — Recognition

L1.1

Match each term to its role: (a) Dockerfile, (b) image, (c) container, against the roles (i) running isolated process, (ii) frozen read-only template, (iii) build recipe.

Recall Solution
  • (a) Dockerfile ↔ (iii) build recipe.
  • (b) image ↔ (ii) frozen read-only template.
  • (c) container ↔ (i) running isolated process.

Analogy anchor: recipe → cake mould → the cake you actually eat.

L1.2

In docker run -p 8080:8000 myapp:1.0, which number is the host port and which is the container port?

Recall Solution

The pattern is -p host:container. So 8080 = host (your machine), 8000 = container (inside). Traffic hitting localhost:8080 is forwarded to port 8000 inside the container. Left is always the outside; right is always the inside.

L1.3

True or false: EXPOSE 8000 opens port 8000 to the outside world.

Recall Solution

False. EXPOSE is metadata / documentation only — it records the port the app intends to use. Actual publishing happens with -p host:container at docker run. Without -p, nothing outside can reach the app.


Level 2 — Application

L2.1

Write a minimal Dockerfile that (a) starts from a slim Python 3.11 base, (b) sets /app as the working directory, (c) installs deps from requirements.txt, (d) copies the source, (e) runs python app.py. Order the layers to maximise cache reuse.

Recall Solution
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]

Why this order? Copy requirements.txt alone before installing so the expensive pip install layer is cached and only re-runs when dependencies change. Copy the full source (COPY . .) last, because source changes every commit — putting it last keeps (the first changed layer) as low down the file as possible.

L2.2

You built myapp:1.0. Write the command to run it so that localhost:5000 on your laptop reaches the app listening on port 8000 inside.

Recall Solution
docker run -p 5000:8000 myapp:1.0

-p host:container → host 5000 maps to container 8000. Now http://localhost:5000 forwards into the container's port 8000.

L2.3

Your model process writes model.pkl at runtime and you need it to survive after the container is deleted. Write the run command that mounts the host folder ./data into the container's /app/data.

Recall Solution
docker run -v ./data:/app/data myapp:1.0

The -v host:container flag mounts a volume. A container's own writable layer is ephemeral (destroyed on removal), so anything that must persist has to live in a mounted volume on the host.


Level 3 — Analysis

L3.1

A build has 5 layers with costs (seconds): Layer 4 (COPY . .) changes because you edited source. Using , compute the rebuild time.

Recall Solution

The first changed layer is . Layers 1–3 are cache hits (cost 0). Sum from to : The big (pip install) is above , so it is skipped entirely. Good ordering paid off.

L3.2

Same costs as L3.1, but you now instead have COPY . . as layer 2 (before the install). You edit source. What is the rebuild time, and why is it so much worse?

Recall Solution

Now the first changed layer is . Sum from to : Because the pip-install layer sits below the code copy, a code edit invalidates it too. Same edit, 95 s vs 4 s — a slowdown purely from ordering.

Figure — Containerization with Docker

L3.3

An image shares its base layer (200 MB) with 4 other images on the same host. Each image also has a unique 50 MB app layer. How much disk do all 5 images consume together, given content-addressed layer sharing? Compare to the naive "5 × full size" assumption if each full image were 250 MB.

Recall Solution

Layers are content-addressed by hash, so the shared 200 MB base is stored once, not five times.

  • Shared base stored once: MB.
  • Five unique app layers: MB.
  • Total actual: MB.
  • Naive assumption (): MB.
  • Saved: MB. Sharing turns "5 copies of the base" into "1 copy," which is why a common base image is so cheap across many services.

Level 4 — Synthesis

L4.1

Design a Dockerfile for a FastAPI model-serving app (from Model Serving with FastAPI) that: uses a slim base, pins dependencies, keeps the pip layer cached across code edits, documents port 8000, and launches with uvicorn. Make it a proper CLI-style container so docker run img --port 9000 can override the port.

Recall Solution
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
ENTRYPOINT ["uvicorn", "app:app", "--host", "0.0.0.0"]
CMD ["--port", "8000"]

Why the ENTRYPOINT + CMD split? ENTRYPOINT fixes the executable (uvicorn app:app --host 0.0.0.0), while CMD supplies default arguments (--port 8000) that are replaced when you pass args at run time. So docker run img --port 9000 swaps the port without editing the image — the container behaves like a real CLI tool. Requirements are copied and installed before the source so the pip layer stays cached.

L4.2

Your final image is 1.1 GB. It came from FROM python:3.11 (full). List two ordering/base changes that shrink it, and state which mistake each fixes.

Recall Solution
  1. Switch to python:3.11-slim (or distroless / multi-stage). Fixes the "use the full image to be safe" trap: the full base ships ~1 GB of build tools you never run in production — larger, slower to pull, bigger attack surface.
  2. Add --no-cache-dir to pip install and copy only what you need. Fixes bloated layers caused by pip's download cache being baked into the image. Both keep the shipped runtime image minimal. (Multi-stage builds go further: compile in a fat builder stage, copy only artifacts into a slim final stage.)

Level 5 — Mastery

L5.1

A teammate reports: "The container ran fine, trained a model, wrote best_model.pkl, exited. I restarted with docker run again and the file is gone." Diagnose the cause and give the exact fix, distinguishing container writable layer from a volume.

Recall Solution

Cause: the model was written into the container's ephemeral writable layer, which is discarded when the container is removed. Each fresh docker run starts a brand-new container from the unchanged image, so the previous run's .pkl does not exist. Fix: mount a volume so writes land on the host, not the ephemeral layer:

docker run -v ./artifacts:/app/artifacts myapp:1.0

Now best_model.pkl written to /app/artifacts persists on the host under ./artifacts and is available to every future run.

L5.2

Reproducibility audit. You have two images, svc:a and svc:b. svc:a used FROM python:3.11-slim and scikit-learn==1.4.0; svc:b used FROM python:latest and unpinned scikit-learn. Six months later, which image is guaranteed to reproduce its original behaviour, and precisely why? Then explain how CI-CD Pipelines and Container Registries make this guarantee even stronger.

Recall Solution

svc:a reproduces; svc:b does not.

  • svc:a pins an immutable base tag and an exact library version, so rebuilding produces the same environment — same OS libs, same Python, same scikit-learn.
  • svc:b uses latest (mutable — may now point to Python 3.13) and unpinned scikit-learn (may resolve to a newer, behaviour-changing release). Rebuilds are non-deterministic.
  • CI/CD builds the image once and pushes an immutable-tagged artifact (git SHA) to a registry. From then on you pull that exact image instead of rebuilding, so even the original build inputs no longer matter — the frozen bytes are what deploys. That is the strongest reproducibility guarantee: ship the image, not the recipe.

L5.3

Cost-model decision. You edit source ~30 times/day. Ordering A (deps-before-code) has with s per rebuild; ordering B (code-before-deps) has with s per rebuild (from L3). How much developer time per day does the correct ordering save, and per 5-day week?

Recall Solution

Per rebuild saving: s.

  • Per day ( rebuilds): s minutes.
  • Per 5-day week: s minutes hours. Almost four hours a week reclaimed by a single line-ordering choice — the derivation from turned into real payroll time.

Active recall

Recall Rapid review (answer before expanding)
  1. In -p 3000:8000, which is the container port?
  2. Where must layers that change every commit sit — top or bottom of the Dockerfile? Why?
  3. If layer 3 of 6 changes, which layers rebuild?
  4. Why does latest break reproducibility?
  5. What survives container deletion?

Answers: 1) 8000 (right side = inside). 2) Bottom, to keep low so fewer layers rebuild. 3) Layers 3, 4, 5, 6 — from down to . 4) It's a mutable tag; it can point to a different image later. 5) Only data on a mounted volume.

Which level number tests designing a full Dockerfile from scratch?
L4 Synthesis
Which formula gives rebuild cost?
, summing layer (first changed) down to the last layer

Connections