11 Docker Containers
11.2.3 Images vs. containers: the distinction everything builds on |
11.4 The Machinery: Engine, Registries, and the Container Lifecycle |
11.6.3 Build context, .dockerignore, and the cache-order trick |
11.1 Learning Objectives
By the end of this lecture you should be able to:
Explain the problem containers solve—
“works on my machine”— and contrast containers with virtual machines along the isolation/weight axis. Distinguish images from containers (class vs. instance) and manage both with the core CLI: pull, run, ps, logs, exec, stop, rm.
Read and write a Dockerfile—
FROM, WORKDIR, COPY, RUN, ENV, EXPOSE, CMD, ENTRYPOINT— and order its instructions to exploit layer caching. Explain image layers, tags, and pinning, and why COPY requirements.txt before COPY . . changes rebuild time from minutes to seconds.
Persist data across container restarts with named volumes, and choose between volumes, bind mounts, and tmpfs.
Wire multi-container applications with Docker networks and Compose: service DNS, port publishing, depends_on with health checks, and .env configuration.
Debug a misbehaving container with logs, exec, inspect, and stats, following a systematic checklist.
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—
“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 environment—
Containers fix this by shipping the environment with the
code. A container image bundles your app plus its entire
userspace—
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—
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 on—
run 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 format—
the 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—
11.3 Running Example: TerpTasks Ships
The arc of the lecture, in four checkpoints:
Run someone else’s image (nginx) to learn the container lifecycle—
no code of ours involved. Containerize TerpTasks’ API with a Dockerfile—
one artifact any grader can run. Give it real infrastructure—
PostgreSQL with a persistent volume, Redis for a cache— wired by Compose so docker compose up boots the whole stack. Ship it—
build, test, and push the image to GitHub Container Registry from CI, closing the loop the git lecture opened.
The final stack—
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—
11.4.1 Worked example: the whole lifecycle with nginx
Checkpoint 1—
docker run -d --name web -p 8000:80 --restart unless-stopped nginx:1.27Unpack 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. instanceStep 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—
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—
Base images and tags. Every image starts FROM
another—
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
code—
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”—
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—
This works. It is also slow in a way you’ll feel
immediately—
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—
The cache. docker build reuses a cached layer when the
instruction and everything it depends on are unchanged—
The fix—
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”—
Same image, same behavior—
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 instance—
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—
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 containerTrace 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—
The solution. Docker networks. Containers attached to the
same user-defined bridge network can reach each other,
and—
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-apiNote what is and isn’t exposed: api publishes port 8000 to the
host (-p); db and cache publish
nothing—
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—
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—
# 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, cache—
Environment variables and secrets, the rules: configuration
that varies by environment (DATABASE_URL, ports) rides in
environment variables—
depends_on and the classic race: plain depends_on
waits for the container to start, not the database
inside it to accept connections—
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 detectorWorked micro-example. curl localhost:8000 hangs.
Checklist: ps says the container is up (not it). logs
shows uvicorn bound to 127.0.0.1:8000—
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—
11.10.3 Registries and CI/CD
An image on your laptop helps nobody. Push it to a
registry—
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.0And close the git lecture’s loop—
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 digest—
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—
11.11 GitHub-Ready Mini-Project: terptasks-docker
See the exercise repository on GitHub.
11.12 Practice Exercises
11.12.1 Basic
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.
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.
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.
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?
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
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?
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.
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.
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?
E10. Persist the Redis counter across restarts (Redis supports persistence with a volume + config flag). Decide first whether you should—
write three sentences on which data belongs in cache vs. database, then implement it anyway to prove you can.
11.12.3 Advanced
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.
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. 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
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.
Containers ≠ VMs: shared kernel, namespace isolation, millisecond starts. An image is the class; a container is the instance with a disposable writable layer.
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. RUN is build time; CMD is run time. EXPOSE documents; -p connects. 127.0.0.1 inside a container is its loopback, not yours.
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. Networks + DNS make service names hostnames (db:5432), and unpublished ports are a security feature, not an oversight.
Compose is the stack as one reviewable file—
services, volumes, networks, env from .env (gitignored; commit .env.example), health-checked startup order. Debug systematically: ps → logs → exec → inspect → stats. 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 logs → exec → inspect.
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—
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. container—
it 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 confusion—
both directions: 127.0.0.1 binding inside, and “why can’t the host see db:5432.” E4 targets exactly this pair. depends_on optimism—
students assume started = ready. Break it live (E6b) and let them watch the crash loop. Cache order apathy—
until 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).
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.
The cache reorder race: two Dockerfiles, one-line code edit, time docker build both. Minutes vs. ~a second.
The whole-stack boot: docker compose up from a clean clone to passing smoke test in under two minutes, narrated against the compose file.
Course tie-in: ask Claude Code to review the naive Dockerfile—
it 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—
Homework ideas. E6+E7+E8 as the standard set (debugging
drills, dev workflow, image forensics—
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—
1Factor III (“Config”) of the
Twelve-Factor App, a set of
principles for deployable services published in 2011 by Adam Wiggins
at Heroku—