By now your capstone project has a model, pipeline, or agent that runs on your machine. This week's job is to make it run identically everywhere else — a teammate's laptop, a CI runner, a cloud VM. The tool for that is the container. A container packages your application with everything it needs to run: the language runtime, system libraries, your code, and a locked dependency list. Because it carries its own environment instead of depending on whatever's installed on the host, it runs the same way regardless of where it's launched. That's the reproducibility and dev/prod parity this whole certificate has built toward — the image you test locally is the exact image that runs in production. No "works on my machine." Multi-stage builds are the single highest-leverage optimization you can make to a Dockerfile. A naive Dockerfile installs compilers, build tools, dev headers, and source code into one image and ships all of it to production — even though none of that is needed once the app is built. A multi-stage build fixes this with more than one FROM statement in the same Dockerfile:
| Stage | Base image | Purpose | What leaves the image |
|---|---|---|---|
builder |
Full build image (e.g. python:3.12) |
Installs build tools, resolves/compiles dependencies | Nothing — this stage is discarded |
runtime |
Minimal base (e.g. python:3.12-slim) |
Copies over only the installed packages and app code | This is the image you ship |
The final FROM becomes your shipped image; earlier stages are copied in selectively with COPY --from=builder. The effect is dramatic: images that would otherwise be hundreds of megabytes routinely shrink to tens of megabytes. Smaller images pull and start faster, and — just as important — have a far smaller attack surface, since no compiler or dev-tool shell sits in production waiting to be abused if the container is ever compromised. Use uv inside the Dockerfile to make the Python side reproducible and fast. Instead of a loose pip install -r requirements.txt, COPY your pyproject.toml and uv.lock into the builder stage and run uv sync --frozen. The --frozen flag refuses to update the lockfile — it installs exactly the versions your team tested, every time, on every machine. Because Docker layer caching means the dependency-install layer only reinvalidates when uv.lock changes, builds stay fast even as the project grows. This week's lab ties it together: a Dockerfile for your capstone's core service (likely FastAPI wrapping your model or pipeline), plus a docker-compose.yml that brings up your full local stack — API and datastore — with one command. Correcting three common misconceptions:
.env file read by Compose, or a secrets manager. Never bake secrets into a build stage.USER (create one with useradd, then USER appuser before CMD), avoid --privileged, and grant only the capabilities the service actually needs — for a FastAPI prediction endpoint, essentially none.:latest is convenient, and shipping build tools in the final image doesn't hurt." :latest is a moving target that can silently swap in a breaking version. Pin explicit tags (e.g. python:3.12.5-slim). And shipping compilers or source in the runtime image only bloats size and attack surface — multi-stage builds are exactly what prevents this.Get comfortable with this pattern now — every remaining capstone week assumes your core service already runs in a container.