Multi-stage builds, image-size optimization, and Python-specific patterns.
Cloud & DevOps Module 1
| Aspect | 180 MB image | 90 MB image |
|---|---|---|
| Cold-start pull (CI runner) | ~5 s | ~2 s |
| Registry storage cost | $$ × N tags | $ × N tags |
| Surface area for CVEs | larger | smaller |
| K8s rolling-update time | slower | faster |
The compilers and headers are scaffolding. You wouldn’t ship scaffolding with the building.
“Build in a heavy image with all the dev tools. Copy just the artefacts into a clean runtime image. Discard the heavy stage.”
FROM in a Dockerfile starts a new stage with its own filesystem# Stage 1: build
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2: runtime
FROM python:3.11-slim
COPY --from=builder /install /usr/local
WORKDIR /app
COPY app.py .
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
AS builder — names the stage so the next FROM can reference it--prefix=/install — keeps installed packages in a controlled locationCOPY --from=builder — the magic: brings only the artefacts forwardpip install --prefix vs virtualenv vs --target| Method | Install path | Use when |
|---|---|---|
--prefix=/install | /install/lib/python3.X/site-packages | Simple API — copy prefix to /usr/local in runtime stage |
--target=/install | /install/... directly | Distroless / from-scratch images; add to PYTHONPATH |
| virtualenv | /opt/venv/... | Python version juggling, multi-env images |
Recommendation for this course: --prefix for slim-based images, --target for distroless.
# syntax=docker/dockerfile:1.7
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --prefix=/install -r requirements.txt
DOCKER_BUILDKIT=1 (default in Docker Desktop 23+) enables the syntaxuvFROM python:3.11-slim AS builder
RUN pip install --no-cache-dir uv
WORKDIR /build
COPY requirements.txt .
RUN uv pip install --system --prefix=/install -r requirements.txt
uv is Astral’s Rust-based pip replacement--system skips uv’s automatic venv creation when using a prefixpython:3.11-slim — and why “slim” still isn’t slim/bin/sh, /bin/bash)/etc/passwd machinery, locale data, certificatesFor most Python apps you don’t need apt at runtime. You need Python + your wheels.
FROM python:3.11-slim AS builder
# ... build into /install ...
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /install /usr/local
WORKDIR /app
COPY app.py .
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
docker exec ... /bin/sh — use docker exec ... python -c "..." instead.dockerignore is mandatory__pycache__/
*.pyc
.venv/
.git/
.pytest_cache/
*.md
tests/
du -sh before and after: often a 10× reduction.git/ history (and possibly .env secrets) get streamed to the daemon.env can end up baked into an intermediate cache layer| Position | Cache cost | Best for |
|---|---|---|
| Early | Stable across many builds | Base image, system packages |
| Middle | Rebuilds on dep change | requirements.txt + pip install |
| Late | Rebuilds on every code edit | COPY app.py |
Place instructions from least-frequently-changing to most-frequently-changing.
python:3.11.9-slim, not python:3.11-slimpip-compile / uv pip compile produces a fully-pinned lockfilepython@sha256:...latest and “what you built last week” is a leading cause of “it worked yesterday”A tag is a mutable pointer. A digest is content-addressed. Only digests guarantee reproducibility.
sample-app/ after this lessonAfter Lesson 02: cloud-devops/sample-app/api/Dockerfile is multi-stage; final image < 100 MB; .dockerignore exists; cloud-devops/sample-app/worker/Dockerfile lands with the same shape.
api/Dockerfile — multi-stage, cache mount, non-root userapi/.dockerignore — newworker/Dockerfileworker/.dockerignore