CSC3119 ยท Module 5 of 9

Containerization, Multi-Stage Docker & Security Hardening

Package microservices with lightweight base images, layer caching, and rootless users

~56 min to study properly

A web API that works on your developer laptop often fails when deployed to cloud servers due to mismatched Python versions, missing C-libraries, or differing OS packages. Docker solves this.

Checkpoint ยท the examiner asks

Explain the architecture of a multi-stage Docker build for a Python ML serving image, and detail two security best practices for production container deployment.

In one minute

Docker packages code, runtime and libraries into immutable layered images so 'works on my laptop' works everywhere. Multi-stage builds compile wheels in a heavy builder stage and copy only the results into a slim base, cutting size by ~80% and removing compilers from the attack surface. Run as a non-root user and use .dockerignore to keep secrets and data out of the image.

Traps examiners look for
  • Shipping gcc and build tools in the production image.
  • Running the container as root.
  • Copying source before installing requirements, which breaks layer caching.
Read

Study notes

01"Works on my machine"

An API that runs on your laptop can fail on a server because of a different Python version, a missing system library (libgomp for XGBoost), different package versions, or environment variables you forgot existed.

A Docker image packages everything the app needs โ€” OS libraries, the Python runtime, pinned packages, your code and model โ€” into one immutable artifact. A container is a running instance of that image. The same image runs identically on your laptop, in CI and in production.

02Layers and caching

Each Dockerfile instruction creates a layer. Docker caches layers and reuses them if the instruction and its inputs haven't changed. Because a change invalidates that layer and every layer after it, order matters:

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .                     # changes rarely
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ app/                              # changes often

With this order, editing your code rebuilds only the last layer (seconds). If you COPY . . before pip install, every code change reinstalls all dependencies (minutes). A .dockerignore keeps .git, .env, notebooks and datasets out of the build context โ€” smaller, faster, and no leaked secrets.

03Multi-stage builds

Some packages need compilers (gcc, g++, build headers) to install. Those tools are hundreds of MB and are pure attack surface once the app is built.

A multi-stage build uses a heavy builder stage to compile, then copies only the results into a clean, minimal runtime stage:

FROM python:3.11 AS builder
COPY requirements.txt .
RUN pip wheel --wheel-dir /wheels -r requirements.txt

FROM python:3.11-slim
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
COPY app/ app/

Typical result: a ~1.2 GB image becomes ~150โ€“200 MB โ€” faster to push, pull and start, and with no compilers for an attacker to use. Base image choice matters too: python:3.11-slim (Debian, glibc) is the safe default; Alpine is tiny but uses musl, so NumPy/XGBoost wheels often must compile from source โ€” slow builds and subtle bugs.

04Run as non-root, and other hardening

By default a container's process runs as root. If an attacker exploits a vulnerability in your code, a dependency โ€” or a malicious pickle โ€” they get root inside the container, which makes escaping to the host or tampering with mounted volumes much easier.

RUN useradd --create-home appuser
USER appuser
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Other hardening worth naming in an exam: pin base image versions (or digests), scan images for CVEs (Trivy, Docker Scout), keep secrets out of the image (inject them as environment variables at runtime), add a HEALTHCHECK, and keep the image minimal.

Watch it done

Worked examples

Worked example 1

How much did the multi-stage build save?

A single-stage ubuntu-based image is 1,315 MB (850 MB base, 420 MB build tools, 45 MB app). Rebuilt multi-stage on python:3.11-slim (140 MB) it's 185 MB. What's the reduction?

    Worked example 2

    Why does every build take 2 minutes?

    A Dockerfile has: FROM python:3.11-slim โ†’ COPY . . โ†’ RUN pip install -r requirements.txt โ†’ CMD โ€ฆ. Each one-line code change triggers a full reinstall. Why, and how do you fix it?

      Vocabulary

      Key terms

      Why Alpine Linux Can Slow Python Builds

      Alpine Linux uses musl libc instead of glibc. Because most scientific Python packages (NumPy, SciPy, pandas) publish pre-compiled wheels for glibc (manylinux), Alpine forces slow compilation from source during build, often taking 20+ minutes and creating larger final images than python-slim.

      Practise

      Quick check

      0 of 8 correct so far. Wrong answers come with an explanation โ€” and the buddy can unpack them.

      Q1Why place 'COPY requirements.txt .' and 'RUN pip install' before 'COPY . .' in a Dockerfile?

      Q2Which base image provides the best balance of minimal size and compatibility for PyTorch/Scikit-Learn microservices?

      Q3What does a multi-stage build mainly remove from the final image?

      Q4Why copy requirements.txt before the application code?

      Q5Why can python:3.11-alpine make ML image builds slower?

      Q6What does USER appuser protect against?

      Q7What belongs in .dockerignore?

      Q8Where should the GEMINI_API_KEY go?

      Exam-style

      Written practice

      Short answer5 marks

      Explain how Docker layer caching works and how Dockerfile order affects build time.

      Short answer4 marks

      Why should production containers never run as root?

      Long answer12 marks

      Write a production Dockerfile for the stroke-risk FastAPI service and explain each decision (base image, stages, caching, security, configuration, health).

      Watch

      Videos

      Fresh picks are chosen by AI every day.
      Hands-on

      Docker Multi-Stage Build & Image Layer Optimizer

      4 guided tasks: predict, experiment, explain โ€” then challenge yourself. Plus 1 coding exercise you run right in the browser.

      Start the lab