Module 9 · Deployment and operations

Lesson 40 — Docker

Images, multi-stage Dockerfiles and compose for TicketFlow development.

Published
In this lesson
  1. Exercise 1 — The Dockerfile
  2. Exercise 2 — The secret and the CVE
  3. Exercise 3 — The compose
  4. Exercise 4 — Migrations at boot
  5. Exercise 5 — The production image
  6. Submit

TicketFlow's image, grown up. No solutions.md before submitting.

Exercise 1 — The Dockerfile

  1. Write the complete multi-stage Dockerfile (builder + runtime, non-root user, healthcheck) and compare against your previous image: docker images — how many MB did it save?
  2. The non-root user: run docker run --rm ticketflow whoami (expected: uid 10001) and demonstrate it CANNOT write to /usr (docker run --rm ticketflow touch /usr/x → Permission denied). Why does this matter with 22 in hand?
  3. Reproducibility: build twice and compare the image IDs. Change ONE code comment and rebuild: does the hash change? Which layer got invalidated and which survived (docker build with the cached-steps output)?

Exercise 2 — The secret and the CVE

  1. The secret test: docker history ticketflow --no-trunc | grep -iE "secret|password|key" → empty? And add the .dockerignore (.env*, .git, tests/, db.sqlite3). Break it on purpose: do a COPY.. WITHOUT the dockerignore with a .env present and find the secret with docker export/docker history. Document the finding.
  2. The scan: trivy image ticketflow:latest (or docker scout cves). List the base layer's top 3 CVEs. Which one is "real" (exploitable in your context) and which is noise? Write the triage criterion in 3 lines.
  3. The pinned digest: change python:3.12-slim to python:3.12-slim@sha256:... (your current version's digest) and verify the build stays identical. Which disaster does pinning avoid?

Exercise 3 — The compose

  1. Assemble the complete compose (db, redis, web, worker, beat with healthchecks and service_healthy) and bring it up: docker compose up — paste the boot log showing web WAITED for db's health.
  2. Hot-reload: edit a view from your editor and verify runserver reloads inside the container (the bind mount). Then: edit requirements.txt and rebuild: did the pip cache survive a code change? (the COPY order).
  3. The worker on the same image: docker compose run --rm worker shell — a shell inside the full environment (DB, Redis). What do you use it for day to day (24's shell without a request)?

Exercise 4 — Migrations at boot

  1. Break it on purpose: command: sh -c "python manage.py migrate && runserver" and scale docker compose up --scale web=3. Watch the 3 migrates compete (logs). Which lock saved it from corrupting (10), and what annoyance does it generate?
  2. The grown-up pattern: remove migrate from web, run docker compose run --rm web python manage.py migrate ONCE (the deployment's job) and then scale to 3 replicas: clean boots with no migrate.
  3. The clean-boot proof: kill web (docker compose restart web) and time until the first 200 on /healthz/. What does the boot wait for, and what doesn't it wait for?

Exercise 5 — The production image

  1. Size: list the top 5 directories with docker export | du (or dive if you have it): what is heaviest and what would you shave? Is the effort worth it (distroless for real, or is slim enough)?
  2. The E2E compose (35): reuse the compose with command: pytest tests/e2e and a data seed: how long does the full environment take from zero (build+up+ready)? The metric deciding whether E2E runs on every PR (41).
  3. Write the ADR (48) "TicketFlow's image": multi-stage slim, non-root user, pinned digest, config per environment. The 3 reasons and the discarded alternative (one giant image? a base VM?). 10 lines.

Submit

Paste the Dockerfile with its MB, the non-root and secret evidence, the service_healthy log and the image ADR. Next: Lesson 41 — CI/CD.