On this page:
11.1 Learning Objectives
11.2 Motivation:   “Works on My Machine”
11.2.1 The oldest bug in team software
11.2.2 Containers vs. virtual machines
11.2.3 Images vs. containers:   the distinction everything builds on
11.3 Running Example:   Terp  Tasks Ships
11.4 The Machinery:   Engine, Registries, and the Container Lifecycle
11.4.1 Worked example:   the whole lifecycle with nginx
11.5 Images:   Layers, Caching, and Tags
11.6 The Dockerfile:   Your Environment as Code
11.6.1 The instructions
11.6.2 Terp  Tasks’ Dockerfile (naive draft)
11.6.3 Build context, .dockerignore, and the cache-order trick
11.7 Volumes:   Where Data Survives
11.8 Networking:   How Containers Find Each Other
11.9 Docker Compose:   The Stack as One File
11.10 Debugging and Best Practices
11.10.1 The debugging toolkit
11.10.2 Production hygiene
11.10.3 Registries and CI/  CD
11.10.4 A glimpse of Kubernetes
11.11 Git  Hub-Ready Mini-Project:   terptasks-docker
11.12 Practice Exercises
11.12.1 Basic
11.12.2 Intermediate
11.12.3 Advanced
11.13 Summary
11.13.1 Key takeaways
11.13.2 Terminology
11.13.3 Common mistakes
11.13.4 Connections
11.14 Instructor Notes
9.1

11 Docker Containers🔗

    11.1 Learning Objectives

    11.2 Motivation: “Works on My Machine”

      11.2.1 The oldest bug in team software

      11.2.2 Containers vs. virtual machines

      11.2.3 Images vs. containers: the distinction everything builds on

    11.3 Running Example: TerpTasks Ships

    11.4 The Machinery: Engine, Registries, and the Container Lifecycle

      11.4.1 Worked example: the whole lifecycle with nginx

    11.5 Images: Layers, Caching, and Tags

    11.6 The Dockerfile: Your Environment as Code

      11.6.1 The instructions

      11.6.2 TerpTasks’ Dockerfile (naive draft)

      11.6.3 Build context, .dockerignore, and the cache-order trick

    11.7 Volumes: Where Data Survives

    11.8 Networking: How Containers Find Each Other

    11.9 Docker Compose: The Stack as One File

    11.10 Debugging and Best Practices

      11.10.1 The debugging toolkit

      11.10.2 Production hygiene

      11.10.3 Registries and CI/CD

      11.10.4 A glimpse of Kubernetes

    11.11 GitHub-Ready Mini-Project: terptasks-docker

    11.12 Practice Exercises

      11.12.1 Basic

      11.12.2 Intermediate

      11.12.3 Advanced

    11.13 Summary

      11.13.1 Key takeaways

      11.13.2 Terminology

      11.13.3 Common mistakes

      11.13.4 Connections

    11.14 Instructor Notes

11.1 Learning Objectives🔗

By the end of this lecture you should be able to:

  1. Explain the problem containers solve—“works on my machine”—and contrast containers with virtual machines along the isolation/weight axis.

  2. Distinguish images from containers (class vs. instance) and manage both with the core CLI: pull, run, ps, logs, exec, stop, rm.

  3. Read and write a Dockerfile—FROM, WORKDIR, COPY, RUN, ENV, EXPOSE, CMD, ENTRYPOINTand order its instructions to exploit layer caching.

  4. Explain image layers, tags, and pinning, and why COPY requirements.txt before COPY . . changes rebuild time from minutes to seconds.

  5. Persist data across container restarts with named volumes, and choose between volumes, bind mounts, and tmpfs.

  6. Wire multi-container applications with Docker networks and Compose: service DNS, port publishing, depends_on with health checks, and .env configuration.

  7. Debug a misbehaving container with logs, exec, inspect, and stats, following a systematic checklist.

  8. Apply production practices—small multi-stage images, non-root users, pinned versions—and ship an image through GitHub Actions to a registry.

11.2 Motivation: “Works on My Machine”🔗

11.2.1 The oldest bug in team software🔗

TerpTasks is a real service now: FastAPI routes ( REST APIs and FastAPI: Designing and Building Web Interfaces lecture), PostgreSQL storage (Relational Database Design & MySQL lecture), clean layers (Layered Architecture lecture). Time to deploy it—and the intern volunteers. Monday’s report:

“It works on my machine. The grader’s laptop is missing psycopg, the TA has Python 3.9 and our type hints crash, the server has PostgreSQL 12 but we developed on 17, and something called libpq is the wrong version on Alice’s Mac.”

Nothing is wrong with the code. What’s wrong is that the code silently depends on an environmentinterpreter version, system libraries, OS packages, config—and every machine’s environment is a unique, hand-grown snowflake. The requirements.txt pins Python packages, but nothing pins everything else.

Containers fix this by shipping the environment with the code. A container image bundles your app plus its entire userspace—interpreter, libraries, OS files—into one artifact that runs identically on every machine with a container runtime. The unit of deployment stops being “code plus a prayer” and becomes a sealed box.

11.2.2 Containers vs. virtual machines🔗

A container is a set of processes running on the host’s kernel, isolated by Linux namespaces (own process tree, network stack, filesystem view) and resource-limited by cgroups. A virtual machine emulates hardware and boots a complete guest operating system, kernel included.

      CONTAINERS                          VIRTUAL MACHINES

+-----+ +-----+ +-----+             +---------+ +---------+

| app | | app | | app |             |   app   | |   app   |

| libs| | libs| | libs|             |  libs   | |  libs   |

+--+--+ +--+--+ +--+--+             | guest OS| | guest OS|

+--+-------+-------+--+             | (kernel)| | (kernel)|

|   container engine  |             +---------+ +---------+

+---------------------+             +---------------------+

|  host OS (1 kernel) |             |      hypervisor     |

+---------------------+             +---------------------+

|       hardware      |             |       host OS       |

+---------------------+             +---------------------+

                                    |       hardware      |

                                    +---------------------+

 starts in milliseconds,             starts in minutes,

 megabytes per instance              gigabytes per instance

Intuition. A VM is a house—own foundation, plumbing, everything. A container is an apartment—private rooms and its own front-door lock, but shared building infrastructure (the kernel). Apartments are cheaper and faster to move into; houses isolate more strongly. Because containers share the host kernel, you can run dozens on a laptop that would struggle with three VMs—which is exactly what makes the multi-service development story practical.

Common misconceptions.

  • “A container is a lightweight VM.” Close enough for day one, wrong afterward: there is no guest kernel. A container is processes with blinders onrun ps on the host and you can see your container’s processes sitting right there.

  • “Docker invented containers.” Namespaces and cgroups are Linux kernel features; Docker’s contribution was packaging them behind a humane UX and, crucially, the image formatthe shippable artifact.

  • “Containers are only for deployment.” Half their value is development: PostgreSQL 17 and Redis on your laptop in one command, deleted without residue when the semester ends.

11.2.3 Images vs. containers: the distinction everything builds on🔗

An image is an immutable, layered filesystem snapshot plus metadata (default command, environment, exposed ports). A container is a running (or stopped) instance of an image, with its own writable layer on top.

In plain English—the class and the instance. python:3.12 is a class; docker run python:3.12 news one up. You can instantiate the same image ten times and get ten independent containers; deleting a container never touches the image, and changes inside a container never touch the imagethat writable layer is the container’s own (and Volumes: Where Data Survives shows it evaporating).

11.3 Running Example: TerpTasks Ships🔗

The arc of the lecture, in four checkpoints:

  1. Run someone else’s image (nginx) to learn the container lifecycle—no code of ours involved.

  2. Containerize TerpTasks’ API with a Dockerfile—one artifact any grader can run.

  3. Give it real infrastructurePostgreSQL with a persistent volume, Redis for a cache—wired by Compose so docker compose up boots the whole stack.

  4. Ship itbuild, test, and push the image to GitHub Container Registry from CI, closing the loop the git lecture opened.

The final stack—the standard shape of a modern web service:

browser --> api (FastAPI container, port 8000)

              |                    |

              v                    v

     db (postgres:17)      cache (redis:7)

     [named volume]        [ephemeral]

11.4 The Machinery: Engine, Registries, and the Container Lifecycle🔗

The pieces, in one paragraph. The Docker Engine is a daemon (dockerd) that builds images and runs containers; the docker CLI just sends it requests. A registry is a server that stores images—Docker Hub is the default public one; GitHub Container Registry (GHCR) is another (Registries and CI/CD). docker pull downloads an image; docker run instantiates it (pulling automatically if needed).

11.4.1 Worked example: the whole lifecycle with nginx🔗

Checkpoint 1—run a real web server without installing a web server:

docker run -d --name web -p 8000:80 --restart unless-stopped nginx:1.27

Unpack every flag, because this one line is half the lecture:

Piece

Meaning

-d

detached: run in the background (omit it and the container holds your terminal -- interactive mode, useful with -it for shells)

name web

a human name, instead of an auto-generated one like vigorous_wozniak

-p 8000:80

port mapping: host port 8000 -> container port 80. The container thinks it owns port 80; the host disagrees; this flag is the treaty

restart unless-stopped

restart policy: if the process crashes (or the machine reboots), bring it back -- unless a human stopped it

nginx:1.27

image name and tag

Now the lifecycle, command by command:

curl -s localhost:8000 | head -4      # <!DOCTYPE html> ... Welcome to nginx!
docker ps                             # list running containers: web, Up 2 minutes
docker logs web                       # the requests curl just made, as access logs
docker exec -it web /bin/sh           # a shell INSIDE the container -- look around, exit
docker stop web                       # graceful stop (SIGTERM, then SIGKILL)
docker ps -a                          # -a shows stopped containers too: web, Exited
docker rm web                         # delete the container instance
docker images                         # the nginx IMAGE is still here -- class vs. instance

Step through the mental model: run = instantiate, stop = pause the instance, rm = delete the instance, and none of it touched the image. If you re-run, you get a fresh instance—which is the cliffhanger for Volumes: Where Data Survives, because “fresh” means “whatever the previous instance wrote is gone.”

11.5 Images: Layers, Caching, and Tags🔗

An image is a stack of read-only layers, each recording the filesystem diff produced by one build step. Layers are content-addressed and shared: two images built FROM python:3.12 share those base layers on disk and over the network. A container adds one writable layer on top.

Intuition—Git for filesystems. Each layer is like a commit: a diff on top of its parent. Pulling an image you half-have transfers only the missing layers (watch docker pull say “Already exists” line by line). This is also why image order of construction matters for speed—the build cache (The Dockerfile: Your Environment as Code) works layer by layer, exactly like you’d expect from a commit stack.

Base images and tags. Every image starts FROM another—python:3.12, ubuntu:24.04, postgres:17bottoming out at minimal OS filesystems. The :tag picks a version. Two rules of professional hygiene:

  • Pin your tags. python:3.12-slim today and python:3.12-slim in a year may differ (patch releases), but python:latest may jump a major version between your laptop and CI. latest is a moving target wearing a reassuring name, now in supply-chain form.

  • Prefer official, minimal variants. python:3.12-slim is ~120MB; full python:3.12 is ~1GB of build tools you probably don’t need at runtime; the multi-stage build (Debugging and Best Practices) gets you the best of both.

11.6 The Dockerfile: Your Environment as Code🔗

11.6.1 The instructions🔗

A Dockerfile is a script of instructions that builds an image layer by layer; docker build executes it. It is the environment’s source codeversioned, reviewed, and reproducible, which is exactly the configuration-as-code philosophy from the How to Effectively Use Claude Code lectures applied to infrastructure.

Instruction

Meaning

TerpTasks example

FROM

base image (first line, always)

FROM python:3.12-slim

WORKDIR

cd for all following steps (creates the dir)

WORKDIR /app

COPY

copy files from build context into the image

COPY app/ app/

RUN

execute a command at BUILD time; result = new layer

RUN pip install -r requirements.txt

ENV

set an environment variable (build + runtime)

ENV PYTHONUNBUFFERED=1

EXPOSE

document the port the app listens on (metadata only -- -p does the mapping)

EXPOSE 8000

CMD

default command at RUN time (one per image; run args replace it)

CMD ["uvicorn", ...]

ENTRYPOINT

fixed command prefix; CMD/args become its arguments

rarely needed day one

The RUN vs. CMD confusion, settled: RUN happens once, at build time, and its result is frozen into a layer (installing packages). CMD happens every time a container starts (launching your server). Mixing them up produces images that install dependencies on every boot or bake a single stale server process into the artifact. ENTRYPOINT vs. CMD: ENTRYPOINT is “this image is this program,” CMD is “run this by default”—docker run image othercmd overrides CMD but only appends to ENTRYPOINT.

11.6.2 TerpTasks’ Dockerfile (naive draft)🔗

Checkpoint 2. The minimal shape, applied to our app:

FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

docker build -t terptasks-api .        # -t names (tags) the image; . is the build context
docker run -d -p 8000:8000 terptasks-api

(host 0.0.0.0 matters: inside a container, 127.0.0.1 means the container’s own loopback—a server bound there is unreachable through the port mapping. First debugging classic of the lecture.)

This works. It is also slow in a way you’ll feel immediately—which is the next section.

11.6.3 Build context, .dockerignore, and the cache-order trick🔗

Build context. docker build . ships the entire directory to the daemon first. A .dockerignore file (sibling of .gitignore, same syntax) keeps .venv/, .git/, and __pycache__/ out—smaller context, faster builds, and no secrets accidentally baked into layers.

The cache. docker build reuses a cached layer when the instruction and everything it depends on are unchanged—but a changed layer invalidates every layer after it (a prefix rule—the same shape as prompt caching in the Claude Code lectures, and it should feel familiar). Now look at the naive Dockerfile: COPY . . comes before pip install. Edit one line of main.py → the COPY layer changes → the pip install layer re-runs → every rebuild reinstalls every dependency. Minutes per edit.

The fix—order layers by how often they change:

FROM python:3.12-slim
WORKDIR /app
# requirements change rarely -> this COPY (and the install below) stay cached
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# code changes constantly -> copy it LAST
COPY app/ app/
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

(A syntax gotcha worth learning now: Dockerfile comments must start at the beginning of a line. A trailing # comment after COPY is parsed as extra source arguments and fails the build with a baffling “not found”—one of the classic first-Dockerfile errors.)

Same image, same behavior—but now editing code re-runs only the final COPY. Rebuilds drop from minutes to about a second. This one reordering is the highest-value Dockerfile lesson there is, and the principle—stable content first, volatile content lastyou have now met twice in this course.

11.7 Volumes: Where Data Survives🔗

The problem, dramatized. Run PostgreSQL in a container, create tasks all week, then docker rm the container (an upgrade, a crash, a cleanup). Every row is gone. Why: writes went to the container’s writable layer, and the writable layer is part of the container instancerm the instance, lose the layer. Images are immutable; containers are disposable; therefore anything that must survive needs to live outside both.

A volume is host-managed storage mounted into a container’s filesystem tree. Three kinds:

Kind

Syntax

Lives

Use for

named volume

-v pgdata:/var/lib/postgresql/data

Docker-managed area on the host

databases, anything precious (the default choice)

bind mount

-v $(pwd)/app:/app/app

a directory YOU chose

live code reload in development; sharing config

tmpfs

tmpfs /scratch

RAM only

secrets and scratch that must NOT persist

Worked example—the database that survives:

docker volume create pgdata
docker run -d --name db \
  -e POSTGRES_PASSWORD=terp -e POSTGRES_DB=terptasks \
  -v pgdata:/var/lib/postgresql/data \
  postgres:17

docker exec -it db psql -U postgres -d terptasks \
  -c "CREATE TABLE t(x int); INSERT INTO t VALUES (42);"

docker rm -f db                        # destroy the container. gasp.

docker run -d --name db \
  -e POSTGRES_PASSWORD=terp -e POSTGRES_DB=terptasks \
  -v pgdata:/var/lib/postgresql/data \
  postgres:17                          # new instance, SAME volume

docker exec -it db psql -U postgres -d terptasks -c "SELECT * FROM t;"
#  x
# ----
#  42          <- the data outlived the container

Trace it: the volume mounted over /var/lib/postgresql/data, so PostgreSQL’s writes bypassed the writable layer entirely. Container died; volume didn’t. The mental model to keep: containers are cattle, volumes are the herd’s records. (And the Relational Database Design & MySQL lecture’s migrations run against data that lives in exactly such a volume.)

Misconception. “Bind mounts are for data too.” They work, but you inherit host-path portability problems and permission mismatches. Named volumes for state; bind mounts for development-time code and config.

11.8 Networking: How Containers Find Each Other🔗

The problem. Our API container must reach the database container. Localhost won’t do it—each container has its own localhost (that’s what network namespaces mean).

The solution. Docker networks. Containers attached to the same user-defined bridge network can reach each other, and—the killer feature—Docker runs a DNS service: every container’s name is a hostname. The API doesn’t connect to an IP; it connects to db:5432.

docker network create terpnet
docker run -d --name db    --network terpnet -e POSTGRES_PASSWORD=terp postgres:17
docker run -d --name cache --network terpnet redis:7
docker run -d --name api   --network terpnet -p 8000:8000 \
  -e DATABASE_URL=postgresql://postgres:terp@db:5432/postgres \
  -e REDIS_URL=redis://cache:6379 \
  terptasks-api

Note what is and isn’t exposed: api publishes port 8000 to the host (-p); db and cache publish nothingthey’re reachable only from inside terpnet. That’s the web-container → database-container diagram with a real security property: the database has no door to the outside world at all. (network host exists too—no isolation, container shares the host’s stack; it’s the exception, not the default.)

Misconception. EXPOSE opens ports.” Nothing is reachable from the host without -p. EXPOSE is documentation; -p is plumbing. And between containers on the same network, neither is needed—db:5432 works because of the shared network, full stop.

11.9 Docker Compose: The Stack as One File🔗

Three docker run commands with a dozen flags each is not a development workflow. Compose turns the whole arrangement into a declarative file—checkpoint 3:

# compose.yaml -- the whole TerpTasks stack: one command to boot
services:
  api:
    build: .                        # build from our Dockerfile
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/terptasks
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy  # wait for pg_isready, not just "started"
      cache:
        condition: service_started

  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}   # from .env -- never hardcode
      POSTGRES_DB: terptasks
    volumes:
      - pgdata:/var/lib/postgresql/data          # section 6, as one line
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d terptasks"]
      interval: 2s
      timeout: 2s
      retries: 15

  cache:
    image: redis:7

volumes:
  pgdata:

docker compose up -d       # build + network + volumes + start, in dependency order
docker compose ps          # the stack's status
docker compose logs api    # one service's logs
docker compose down        # stop and remove containers + network (volumes SURVIVE)
docker compose down -v     # ...and only this deletes the volumes. Deliberate.

Walk the file against the previous sections: services: are the three docker runs; Compose auto-creates a network where service names are DNS names (db, cacheexactly what DATABASE_URL uses); volumes: is Volumes: Where Data Survives; environment: + ${POSTGRES_PASSWORD} reads from a .env file sitting next to compose.yaml.

Environment variables and secrets, the rules: configuration that varies by environment (DATABASE_URL, ports) rides in environment variables—this is the twelve-factor convention,1Factor III (“Config”) of the Twelve-Factor App, a set of principles for deployable services published in 2011 by Adam Wiggins at Heroku—before Docker was popular, which is why the two fit so well. Its litmus test: could the codebase be open-sourced right now without leaking a single credential? and it’s why our main.py reads os.environ. Secrets (API_KEY, passwords) go in .env, and .env goes in .gitignorecommit .env.example with placeholder values instead. Never ENV API_KEY=... in a Dockerfile: that bakes the secret into an image layer, and layers are forever (and pushed to registries).

depends_on and the classic race: plain depends_on waits for the container to start, not the database inside it to accept connections—the API would boot first and crash. The condition: service_healthy + healthcheck pattern is the honest fix (plus a small retry loop in the app, because defense in depth).

11.10 Debugging and Best Practices🔗

11.10.1 The debugging toolkit🔗

The systematic checklist when a container misbehaves:

docker compose ps                  # 1. is it even running? exit code?
docker logs api --tail 50          # 2. what did it say before it died?
docker exec -it api /bin/sh        # 3. get inside: is the file there? env set?
docker inspect api                 # 4. the full truth: mounts, env, network, IP
docker stats                       # 5. live CPU/memory -- the OOM detector

Worked micro-example. curl localhost:8000 hangs. Checklist: ps says the container is up (not it). logs shows uvicorn bound to 127.0.0.1:8000found it in two commands: loopback inside a container is unreachable from outside. Fix the CMD, rebuild, done. The point isn’t this specific bug—it’s that the five commands, in order, localize almost everything: crashed vs. running, app error vs. wiring error, config vs. resources.

11.10.2 Production hygiene🔗

Practice

Why

In our project

small images

less to pull, less to attack

python:3.12-slim base

multi-stage builds

build tools don't ship to production

AS builder stage below

don't run as root

container escape shouldn't equal host root

USER app

pin versions

reproducibility (latest is a lie)

python:3.12-slim, postgres:17

official images

maintained, patched, documented

postgres, redis, python

cache-aware layer order

second-long rebuilds

the reordering above

Multi-stage builds, the idea in four lines: stage 1 (FROM ... AS builder) installs compilers and builds dependencies; stage 2 starts from a clean slim base and COPY from=builder only the finished artifacts. Build tools, caches, and compilers never enter the final image—smaller, faster to pull, and with a fraction of the attack surface. The mini-project’s Dockerfile is the complete worked version, non-root user included.

11.10.3 Registries and CI/CD🔗

An image on your laptop helps nobody. Push it to a registrycheckpoint 4:

docker tag terptasks-api ghcr.io/YOURUSER/terptasks-api:v1.0.0   # registry/name:tag
echo $GITHUB_TOKEN | docker login ghcr.io -u YOURUSER --password-stdin
docker push ghcr.io/YOURUSER/terptasks-api:v1.0.0

And close the git lecture’s loop—CI builds and ships the image on every push to main (full workflow file in the project):

push to main --> GitHub Actions: build image --> run smoke tests against it

                     --> push to GHCR --> (deploy pulls the new tag)

The deployment unit is now an image digestthe same sealed box that passed CI is what production runs. “Works on my machine” is dead because there is no longer a “my machine” in the pipeline.

11.10.4 A glimpse of Kubernetes🔗

When one host isn’t enough, Kubernetes orchestrates containers across a fleet: pods (co-scheduled container groups) instead of containers, deployments (“run 5 replicas of this image, always”) instead of docker run, services (stable names load-balancing across replicas) instead of container DNS, ingress instead of -p, plus scaling and rolling updates (replace replicas one at a time; kubectl rollout undo is the alembic downgrade of infrastructure). The conceptual bridge is direct: everything in this lecture is the node-level substrate Kubernetes schedules. Learn Docker well and Kubernetes is vocabulary, not a new worldview. (That’s the whole tour—it’s a topic for after this course.)

11.11 GitHub-Ready Mini-Project: terptasks-docker🔗

See the exercise repository on GitHub.

11.12 Practice Exercises🔗

11.12.1 Basic🔗
  1. E1. Classify each as image-scoped or container-scoped, and justify with the class/instance model: (a) a tag like :v1.0.0; (b) the writable layer; (c) EXPOSE 8000; (d) an entry in docker ps -a; (e) the result of a RUN pip install.

  2. E2. Explain the difference between RUN, CMD, and ENTRYPOINT, and predict what happens to each when a user runs docker run terptasks-api /bin/sh.

  3. E3. A teammate’s Dockerfile has COPY . . before RUN pip install -r requirements.txt. Describe precisely what happens to their rebuild time after editing one comment in main.py, and why. Rewrite the two lines.

  4. E4. The API container can reach db:5432, but psql -h localhost -p 5432 fails on the host. Explain both facts with the networking model. What single line of compose.yaml would make the host connection work, and why might you deliberately not add it?

  5. E5. docker compose down vs. docker compose down -v: what survives each, and which one would have saved the intern who “cleaned up” the grading server?

11.12.2 Intermediate🔗
  1. E6. Run the mini-project. Then break it three ways, observing each with the debugging checklist before fixing: (a) change the CMD host to 127.0.0.1; (b) remove the db healthcheck and the startup retry loop; (c) misspell DATABASE_URL in compose.yaml. For each: which checklist command found it, and what was the telltale sign?

  2. E7. Add a bind mount and reload to a development override file (compose.override.yaml) so editing app/main.py on the host restarts the server inside the container without a rebuild. Explain why you would never ship this override to production.

  3. E8. Measure the multi-stage payoff: build the naive Dockerfile and the multi-stage one; compare docker images sizes and docker history layer counts. Then add one line to each that would leak a secret into a layer, and show with docker history how you’d catch it.

  4. E9. Add a fourth service to the stack: adminer (a one-container database UI) on port 8080, connected to db. No Dockerfile changes allowed. What did you need to know about networking, and what did you not need to configure?

  5. E10. Persist the Redis counter across restarts (Redis supports persistence with a volume + config flag). Decide first whether you shouldwrite three sentences on which data belongs in cache vs. database, then implement it anyway to prove you can.

11.12.3 Advanced🔗
  1. E11. The layer-cache benchmark. Instrument builds with time docker build: cold cache, warm cache with code edit (good ordering), warm cache with code edit (naive ordering), and dependency change (both orderings). Produce the 2×2 table and annotate which cache rule explains each cell.

  2. E12. CI for real. Fork the project to GitHub, enable Actions, and get the workflow green—including the push to GHCR. Then pull your own published image on another machine (or a classmate’s) and run the stack from the registry image instead of building locally (image: instead of build: in compose). Deliverable: the Actions run URL and the cross-machine docker compose ps output.

  3. E13. The container review, EGM-style. Write the four-section design doc for containerizing the full terptasks-rest + terptasks-db stack from the earlier lectures (with Alembic migrations as a startup job—where do migrations run in a containerized world?). The Alternatives section must argue against at least: running migrations in the API’s CMD, and baking seed data into the image. Goldfish-test the doc; deliver doc + critic findings.

11.13 Summary🔗

11.13.1 Key takeaways🔗
  1. Containers ship the environment with the code. The deployable unit becomes a sealed, immutable image; “works on my machine” dies because every machine runs the same box.

  2. Containers ≠ VMs: shared kernel, namespace isolation, millisecond starts. An image is the class; a container is the instance with a disposable writable layer.

  3. The Dockerfile is environment-as-code, and layer order is performance: stable lines first, COPY of volatile code last—the prefix-cache rule you already know from prompt caching.

  4. RUN is build time; CMD is run time. EXPOSE documents; -p connects. 127.0.0.1 inside a container is its loopback, not yours.

  5. State needs a home outside the container: named volumes for databases, bind mounts for dev code, tmpfs for never-persist. down -v is the only thing that deletes volumes—by design.

  6. Networks + DNS make service names hostnames (db:5432), and unpublished ports are a security feature, not an oversight.

  7. Compose is the stack as one reviewable fileservices, volumes, networks, env from .env (gitignored; commit .env.example), health-checked startup order.

  8. Debug systematically: pslogsexecinspectstats. Ship professionally: slim, multi-stage, non-root, pinned, via CI to a registry—the tested artifact is the deployed artifact.

11.13.2 Terminology🔗

Term

Meaning

image / container

immutable layered snapshot / running instance with writable layer

layer

filesystem diff from one build step; content-addressed, shared, cached

registry / tag

image server (Docker Hub, GHCR) / version label (:17, never latest)

Dockerfile

build script: FROM, COPY, RUN, ENV, EXPOSE, CMD, ENTRYPOINT

build context

directory shipped to the daemon; pruned by .dockerignore

build cache

layer reuse until first changed instruction; invalidates all after

port mapping

-p host:container -- the treaty between namespaces

named volume

Docker-managed persistent storage; survives rm and down

bind mount

host directory mounted in; the dev-loop tool

bridge network

user-defined network where container names are DNS hostnames

Compose

declarative multi-container stack: services, volumes, networks

healthcheck/depends_on

readiness probe / ordered startup gated on it

multi-stage build

builder stage discarded; runtime stage ships only artifacts

restart policy

unless-stopped etc. -- crash recovery declared, not scripted

pod/deployment/service

Kubernetes' container group / replica manager / stable LB name

11.13.3 Common mistakes🔗
  • COPY . . before dependency install (every rebuild reinstalls the world).

  • Binding servers to 127.0.0.1 inside the container.

  • Expecting EXPOSE to publish ports, or depends_on to wait for readiness.

  • Database in a container with no volume—one rm from disaster.

  • Secrets in Dockerfile ENV or committed .env (layers and git remember forever).

  • latest tags in anything that matters.

  • Running as root because it was the default.

  • Debugging by rebuild-and-pray instead of logsexecinspect.

11.13.4 Connections🔗
  • Backward: this lecture deploys the whole TerpTasks arc—FastAPI/(part "REST APIs") app, Relational Database Design & MySQL storage (the volume holds those tables; migrations run against it), Layered Architecture boundaries (each layer got its own container). Environment-as-code is CLAUDE.md/(part "Claude Code Files") thinking; layer caching is prompt caching’s prefix rule; CI images extend the git lecture’s pipeline.

  • Forward: Kubernetes when one host isn’t enough; the OWASP notes gain a container chapter (non-root, minimal images, secret hygiene are its first three lines); and agents run in containers too—Claude Code’s sandboxing and the The Elephant-Goldfish Model lab environments are this lecture’s isolation story applied to AI.

11.14 Instructor Notes🔗

Suggested duration: two 75-minute sessions—(A) concepts through volumes; (B) networking through CI—or one intense 100-minute session with registries/Kubernetes as reading.

Segment

Time

works-on-my-machine + containers vs. VMs

12 min

nginx lifecycle, live

12 min

layers, tags, cache mental model

8 min

Dockerfile + the reordering demo, live

15 min

volumes: the survival demo, live

10 min

networking + DNS

10 min

Compose walkthrough

15 min

debugging checklist + best practices

12 min

registry + CI + K8s glimpse

8 min

project + lab kickoff

remainder

Where students struggle.

  • Image vs. containerit feels obvious until docker ps -a shows six dead containers from one image. Keep hammering class/instance; E1 grades it.

  • “My data vanished”the kill-and-resurrect demo preempts the single most common lab disaster. Do it live, theatrically.

  • localhost confusionboth directions: 127.0.0.1 binding inside, and “why can’t the host see db:5432.” E4 targets exactly this pair.

  • depends_on optimismstudents assume started = ready. Break it live (E6b) and let them watch the crash loop.

  • Cache order apathyuntil they feel the rebuild time. The live reordering demo (edit one line, rebuild both variants, wall-clock them) converts more students than any slide.

Live demonstrations (in order of impact).

  1. The volume resurrection (Volumes: Where Data Survives): create data, rm -f the database container in front of the class, bring it back, SELECT the surviving row. Gasps guaranteed.

  2. The cache reorder race: two Dockerfiles, one-line code edit, time docker build both. Minutes vs. ~a second.

  3. The whole-stack boot: docker compose up from a clean clone to passing smoke test in under two minutes, narrated against the compose file.

  4. Course tie-in: ask Claude Code to review the naive Dockerfileit reliably finds the cache ordering, missing .dockerignore, and root user; compare with the class’s list.

Quiz seeds. (1) A container and a VM both isolate applications—name the structural difference and two consequences of it. (2) Walk through what docker run -d name w -p 8080:80 nginx:1.27 does, flag by flag; then state what docker rm w does and does not delete. (3) Why does putting COPY . . before RUN pip install slow every rebuild? State the cache rule and fix the order. (4) Your PostgreSQL container was recreated and all data survived. List the exact mechanism, and the one command that would have deleted it. (5) Inside the Compose stack, the API reaches the database at db:5432, yet EXPOSE appears nowhere on the db service and no -p is set. Explain both. (6) Name three things the multi-stage Dockerfile does that the naive one doesn’t, and the risk each mitigates.

Homework ideas. E6+E7+E8 as the standard set (debugging drills, dev workflow, image forensics—all hands-on). E12 as the flagship: a green Actions run pushing to GHCR is a portfolio artifact. E13 doubles as review for the database and EGM lectures; the “where do migrations run” question sparks the best design discussions of the unit.

TerpTasks completes its journey here: built (FastAPI), specified (REST), persisted (database), structured (layers), and now shipped (containers). The lecture follows the course’s docker-topics.md outline, topics 1–16 in full and 17 as the closing glimpse. Reference: docs.docker.com unusually good documentation; the Dockerfile and Compose references are worth bookmarking.

1Factor III (“Config”) of the Twelve-Factor App, a set of principles for deployable services published in 2011 by Adam Wiggins at Heroku—before Docker was popular, which is why the two fit so well. Its litmus test: could the codebase be open-sourced right now without leaking a single credential?