Docker and containers · Intermediate
A production-minded Dockerfile for a model service
Why it matters
Containers appeared in 84% of the DevOps and 60% of the Software Engineer job descriptions we read. A container image bundles an application with exactly the runtime it needs, so it runs the same on a laptop, in CI and in production. Reviewers look closely at Dockerfiles: they reveal whether you think about security, build speed and reproducibility.
The concept
An image is a stack of read-only layers, one per Dockerfile instruction. A container is a running instance of an image.
Layers are cached. Put things that change rarely (base image, dependencies) before things that change often (your code), so rebuilds are fast.
Pin base images and dependency versions for reproducible builds.
Run as a non-root user: a compromised process then cannot easily take over the container.
Add a health check, so the runtime knows when the service is actually ready.
Keep secrets out of images; pass them at runtime.
Worked example: the propensity model API
The service and model come from Serve a model with FastAPI, and test it on the MLOps shelf (app.py, model.joblib, model.json).
fastapi==0.142.1
uvicorn[standard]==0.37.0
scikit-learn==1.9.1
pandas==3.0.6
joblib==1.5.2# syntax=docker/dockerfile:1
FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py model.joblib model.json ./
RUN useradd --create-home --uid 10001 appuser
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]A .dockerignore keeps the build context small and secrets out:
.git
.venv
__pycache__
*.csv
.envdocker build -t propensity:2026.09.1 .
docker run -d --name prop -p 8000:8000 propensity:2026.09.1
curl localhost:8000/health # {"status":"ok","model_version":"2026.09.1"}
docker inspect --format '{{.State.Health.Status}}' prop # healthy, after the first check
docker exec prop whoami # appuser
docker image ls propensity # about 850 MBScoring the same customer as in the MLOps lesson returns 0.7146 from inside the container: the image reproduces the tested behaviour exactly.
About that size: roughly 850 MB, mostly pandas, NumPy, SciPy and scikit-learn. Options to shrink it: drop pandas if the service can score from plain arrays, export the model to ONNX and serve it with a small runtime, or use a multi-stage build that leaves compilers and caches behind. Measure with docker history propensity:2026.09.1 to see which layer is heavy before optimising.
Common mistakes
COPY . .before installing dependencies. Every code change reinstalls everything. Copy requirements first.Running as root. The default user is root. Add a user and switch to it.
Unpinned
latesttags. Builds change under you. Pin base images and packages.Secrets in images, via
COPY .envorENV API_KEY=.... Anyone who can pull the image can read them. Use runtime environment variables or a secrets manager.
Interview angle
"How would you make this Docker image smaller and safer?" Answer with layer ordering and caching, slim or distroless bases, multi-stage builds, a non-root user, pinned versions, .dockerignore, image scanning (Trivy, Docker Scout) and no secrets in layers. Measure before and after.
Practice task
Rewrite the Dockerfile as a multi-stage build that builds wheels in one stage and installs them in a clean final stage. Compare sizes.
Scan the image with Trivy and fix one finding.
Pass the model version as a build argument and label the image with it.