The Common Belief
What I Actually Did
pyproject.toml changes. The second stage copies the experiment code. The third adds a FastAPI server for serving results. A rebuild after a code change takes 30 seconds, not 10 minutes, because Docker caches the dependency layer.
Layer 2: Docker Compose. Four services:
- An experiment runner (CPU or GPU)
- A results API (FastAPI exposing experiment data as REST endpoints)
- A static dashboard (Nginx serving figures and result tables)
- Shared volumes for results, dataset caches, and CMU-CERT data
The Decisions That Matter
Multi-stage builds exist because rebuilds are expensive
COPY . . before pip install, Docker rebuilds the entire dependency layer every time you change a single line of code. Put the dependency installation first, and Docker's layer cache means changing run_experiments.py doesn't trigger a 10-minute pip install. The principle: things that change frequently go in later layers.
Jobs are not Deployments
Volume mounts separate state from code
Health checks are not optional
livenessProbe and a readinessProbe. Liveness: "is this pod alive?" If it fails three times, Kubernetes kills and restarts it. Readiness: "is this pod ready to receive traffic?" A pod can be alive but not ready (still loading data, for example). Without readiness probes, Kubernetes routes traffic to pods that aren't ready, and users get errors.
Resource limits prevent starvation
requests (guaranteed minimum) and limits (hard ceiling). The API server gets 100m-500m CPU and 128-512Mi memory. The experiment runner gets 2-4 CPUs and 8-16Gi memory. Without limits, one runaway experiment could starve the API server of resources. With limits, Kubernetes schedules pods on nodes that can actually handle them.