Docker Fundamentals
- Explain the difference between an image and a container, and how layers cache
- Write a correct Dockerfile for a Python FastAPI app with a fast, cache-friendly layer order
- Configure ports, volumes, and environment/secrets for a containerized service
- Debug inside a running container and read build output to fix a broken layer
| Spaced-rep warm-up: packaging + OS cards (D17/D39) | 10 min |
| ELI5 + tech read: images/containers/layers, the Dockerfile | 20 min |
| Guided: containerize a FastAPI app + debug from inside | 40 min |
| Practice: audit and fix a bad Dockerfile | 15 min |
| Project: containerize the capstone API | 25 min |
| Quiz + flashcards | 10 min |
Builds on: Day 17 โ Packaging & environments ยท Day 39 โ Operating systems essentials ยท Day 41 โ HTTP & your first API
"It works on my machine" is the oldest lie in software. Your laptop has a specific Python, specific installed libraries, a specific OS, environment variables you set months ago and forgot โ and the server has none of that. So the app that ran perfectly for you dies on deploy, and you spend a day discovering the server had Python 3.9 where you had 3.12.
A container is the fix: instead of shipping just the recipe and hoping the destination kitchen has the right pots, you ship the whole kitchen. A Docker image is a frozen snapshot of a complete filesystem โ a slim Linux, the exact Python, your code, every dependency at pinned versions โ built once from a recipe file called a Dockerfile. A container is a running copy of that image, an isolated process that behaves identically on your laptop, a teammate's Mac, and a cloud server, because it carries its whole world with it. Remember Day 39's processes and Day 17's virtual environments? A container is that idea taken all the way: not just isolated Python packages, but an isolated operating environment, reproducible byte-for-byte. Ship the kitchen, and "works on my machine" becomes "works on every machine."
Every production AI system you will deploy ships as a container โ it is the universal unit of deployment across AWS, GCP, Azure, and every PaaS. Your docs-QA service has a nasty dependency footprint (a web framework, an LLM SDK, a vector store client, an embedding model) and "pip install and pray" on a fresh server is exactly where deploys die. Containerizing makes the environment a reviewed, versioned artifact โ the same discipline as pinning your model ID (Day 141), applied to the whole runtime. For an FDE dropping software into a customer's infrastructure (Day 170), a container is often the *only* thing they'll accept: one image, no host contamination, reproducible in their VPC. Interviewers now assume you can write a Dockerfile; it is table stakes.
Shipping the whole kitchen โ an image builds layer by layer
step 1 / 5A Dockerfile builds bottom-up. Layer 1: the base image โ a minimal OS + Python, identical on every machine.
Guided practice
Containerize a minimal FastAPI app
22 min- In a scratch folder, create a tiny
app/main.pywith a FastAPI app exposingGET /healthreturning{"ok": true}and arequirements.txtwithfastapianduvicorn[standard]. - Create the
Dockerfilefrom the tech section and a.dockerignore. - Build it. terminal:
docker build -t docsqa:dev .โ watch each layer; note which are pulled from cache on a second build. - Run it, publishing the port and passing an env var. terminal:
docker run --rm -p 8000:8000 -e APP_ENV=dev docsqa:dev. - From another terminal: terminal:
curl localhost:8000/healthโ you should get{"ok":true}from inside the container. - Prove the cache rule: edit
main.py(not requirements), rebuild, and confirm the pip layer is reused (CACHED) while only the code layer rebuilds.
Break it, then debug from inside
18 min- Deliberately introduce the classic bug: change the CMD to bind
--host 127.0.0.1. Rebuild, run with-p 8000:8000, and confirmcurl localhost:8000/healthnow hangs or refuses โ the app is only listening inside the container's loopback. - Get a shell in the running container: terminal:
docker exec -it $(docker ps -q -l) /bin/bash. Inside, runcurl localhost:8000/healthโ it works *inside* but not outside. That contrast is the whole lesson. - Fix the host back to
0.0.0.0, rebuild, verify externally. - Second bug: pass no
APP_ENVand confirm the app reports"env":"unset"โ showing config comes from run-time env, not the image. - Inspect the image layers: terminal:
docker history docsqa:devโ read the layer sizes and connect them to your Dockerfile lines.
On your own
Audit a bad Dockerfile
15 minHere is a Dockerfile with at least five problems. Find them, explain the impact of each, and rewrite it correctly.
FROM python:3.12 COPY . . RUN pip install -r requirements.txt ENV ANTHROPIC_API_KEY=sk-ant-abc123 EXPOSE 8000 CMD uvicorn app.main:app --host 127.0.0.1 --port 8000
Goal: name each defect and its consequence (cache busting, image bloat, a secret baked into a layer forever, unreachable binding, shell-form CMD that swallows signals, no .dockerignore, running as root), then produce a corrected version.
Hints: which line reinstalls every dependency on every code change? Which line will leak a credential to anyone who pulls the image? Which line makes the app unreachable from the host?
Containerize the capstone API
Write a production-minded Dockerfile for your real docs-QA service and prove it runs identically in a container. Deliver: a Dockerfile (slim base, dependency layer before code, non-root user, exec-form CMD binding 0.0.0.0), a .dockerignore that excludes secrets, data, and logs, and a short docker/README.md documenting the build and run commands plus every environment variable the container needs. Persist the vector index and logs via a mounted volume so they survive docker rm. Verify: the container answers a real docs-QA query end to end (retrieval + generation) with only env-provided secrets, and a code-only edit rebuilds in seconds thanks to the cache order. Record the final image size and one thing you'd do to shrink it (foreshadowing Day 149's multi-stage builds).
Common mistakes & misconceptions
- Copying code before installing dependencies. Every code edit then busts the pip cache and reinstalls everything โ put COPY requirements.txt and pip install before COPY . .
- Baking secrets into the image with ENV or COPY. Layers are permanent and readable by anyone with the image; pass secrets at run time via --env-file / -e.
- Binding uvicorn to 127.0.0.1 inside the container. It then only listens on the container loopback and is unreachable from the host โ bind 0.0.0.0.
- Using the full python:3.12 base and never cleaning apt lists. Images balloon to a gigabyte; use -slim and rm -rf /var/lib/apt/lists/* (multi-stage on Day 149 goes further).
- Shell-form CMD (CMD uvicorn ...). It runs under /bin/sh which swallows SIGTERM, so the container won't shut down cleanly; use the JSON exec form.
- No .dockerignore. The whole .git history, .env, and local data get copied into the build context and often the image โ slow, bloated, and a leak risk.
Q1. What is the difference between a Docker image and a container?
Q2. Why does `COPY requirements.txt` and `pip install` belong before `COPY . .` in the Dockerfile?
Q3. Your containerized app returns healthy to `curl` run inside the container but refuses connections from the host. Most likely cause?
Go deeper โ curated resources
- Docker โ multi-stage builds (Day 149 preview) โ โ Read how a build stage compiles/installs and a final slim stage copies only the artifacts โ the main lever for shrinking your capstone image tomorrow.
- FastAPI app builds and runs in a container, reachable via curl on the host
- Cache-order rule demonstrated (code edit reuses the pip layer)
- 127.0.0.1 vs 0.0.0.0 bug reproduced and fixed from inside the container
- Capstone Dockerfile + .dockerignore committed; container answers a real query with env-supplied secrets
- Quiz โฅ 2/3
โ Back: This takes Day 17's virtual-environment idea all the way to a reproducible OS-level artifact, using Day 39's process/isolation model and serving the Day-41 FastAPI app you've grown into the capstone.
Forward โ: Day 149 composes this image with a vector DB and cache and shrinks it with multi-stage builds; Day 151 deploys it to the cloud; Day 152 builds and pushes it in CI, and Day 170 ships it into a customer's environment.
Unlocks: D149 Compose, Registries & Image Hygiene ยท D150 Cloud Fundamentals ยท D151 Deploy Lab โ Container to Cloud URL ยท D152 CI/CD with GitHub Actions