Chapter 7 — Reps
Conditioning, not grading. Containers, images, and a model in a box this week.
Ground rules:
- Type every command yourself. No copy-paste of whole blocks, and — Phase 1 rule — build by hand before you bring in AI. When a rep says to generate something with an agent, you must first have built the hand version, so you can judge the agent’s output instead of trusting it.
- Run everything. Read every error. A container error is the kernel naming the exact fence you hit (“permission denied,” “no such device,” “address already in use”). Read it; it is telling you which of namespaces / cgroups / ports / the GPU grant went wrong.
- Predict before you measure. Before each
docker run, say out loud what the port mapping will expose, roughly how big the image is, and whether the data survives adocker rm. Then check. The gap between your guess and reality is the lesson. - Pin versions. Never
:latest. Every image you pull or build gets a real tag. If a rep tempts you toward:latest, that temptation is the rep.
You need Docker (or Podman) installed. Toolchain and environment setup live in Appendix A; local-and-cloud AI setup (Ollama, model pulls) in Appendix B.
Reps 1–3: The Build/Run Loop
Rep 1 — First Image, First Container
Make a folder with this Dockerfile:
FROM python:3.12-slim
WORKDIR /app
RUN echo 'print("shipped, whole, into the world")' > main.py
CMD ["python", "main.py"]
docker build -t hello:0.1 .
docker run --rm hello:0.1
docker image ls hello # how big is it? predict before you look.
Write: What did you predict the image size would be, and what was it? Where did the size come from — your one line of Python, or the python:3.12-slim base layer?
Rep 2 — Ports and the host:container Mapping
Build and run a one-line web server, then prove you understand -p:
docker run --rm -d -p 8080:80 --name web nginx:1.27-alpine
curl -s -o /dev/null -w "%{http_code}\n" localhost:8080 # expect 200
docker stop web
# Now run it WRONG on purpose:
docker run --rm -d -p 8080:9999 --name web nginx:1.27-alpine
curl -s -o /dev/null -w "%{http_code}\n" localhost:8080 # what happens, and why?
docker stop web
Write: In the second run, why does the request fail even though the container is “up”? State the -p HOST:CONTAINER rule in your own words.
Rep 3 — Watch the Layer Cache Work, Then Break It
Use this Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "main.py"]
Build it once (with a real requirements.txt and main.py). Then change one line of main.py and rebuild. Watch which layers say CACHED. Now move COPY . . above the pip install, change main.py again, and rebuild.
Write: In which version did changing your source code force a re-pip install, and why? State the rule for ordering Dockerfile instructions.
Reps 4–6: A Model in a Box
Rep 4 — Ollama in a Container (the easy path)
docker run -d -p 11434:11434 -v ollama-models:/root/.ollama --name ollama ollama/ollama:0.30.10
docker exec ollama ollama pull llama3.2:3b
curl http://localhost:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"llama3.2:3b","messages":[{"role":"user","content":"Summarize Matthew 28:19 in one sentence."}]}'
Write: You hit /v1/chat/completions — the OpenAI-compatible endpoint — on a local model with no OpenAI involved. Why does that matter for portability? What single thing would you change to point the same request at vLLM or llama.cpp instead?
Rep 5 — Lose the Model on Purpose (volumes are not optional)
With the Rep 4 container still around, inspect the volume, then run a second Ollama container without the -v flag:
docker volume ls | grep ollama
docker run -d -p 11435:11434 --name ollama-novol ollama/ollama:0.30.10
docker exec ollama-novol ollama list # empty — no model
docker rm -f ollama-novol # the (nonexistent) model is gone with it
Write: Explain, using the words “writable layer” and “named volume,” why the model persisted across a restart in Rep 4 but would have vanished here. Which AI assets besides weights belong in volumes, not images?
Rep 6 — Put a Face On It (compose, front-end/back-end split)
Use this chapter’s code/compose.yaml:
docker compose -f code/compose.yaml up -d
docker compose -f code/compose.yaml exec ollama ollama pull llama3.2:3b
# open http://localhost:3000 — create the first account, chat with the model
docker compose -f code/compose.yaml ps
docker compose -f code/compose.yaml down
Write: Open WebUI is the front-end; Ollama is the back-end. They meet at one URL inside the compose network — name it. Why is this split (engine serves tokens, UI serves humans) good architecture rather than needless complexity?
Reps 7–9: Build It Right, Review the Agent
Rep 7 — Build the Reference App Image
Build and run this chapter’s code/Dockerfile (it fronts the Ollama backend with code/app.py):
docker build -t verse-api:0.1 code/
docker run --rm -p 8000:8000 -e OLLAMA_URL=http://host.docker.internal:11434 verse-api:0.1
curl -s localhost:8000/health
host.docker.internalonly resolves on Docker Desktop (mac/Windows). On a Linux host — the cloud/no-admin path (Appendix A, Path A) — run with--network=hostand-e OLLAMA_URL=http://localhost:11434, or use the compose service namehttp://ollama:11434. Otherwise the app answers with the 502 it raises when the backend is unreachable.
Write: Name three hardening choices in that Dockerfile (look for the user, the stages, the install order, the healthcheck) and say what bad outcome each one prevents.
Rep 8 — Smoke-Test the Box (it started ≠ it serves)
With the model backend and the app running, run the smoke test in code/smoke_test.sh:
chmod +x code/smoke_test.sh
./code/smoke_test.sh
Write: The script checks three things — app liveness, backend reachability, end-to-end inference. Why is “the container is running” insufficient evidence that the service works? Give a failure that passes a liveness check but fails the inference check.
Rep 9 — Generate a Dockerfile with AI, Then Review It
Now bring in the agent. Ask a coding assistant: “Write a Dockerfile to containerize a FastAPI app in app.py with requirements.txt.” Take whatever it gives you and run the §7.4 review table over it in writing.
Write: For each row of the §7.4 table (base pinning, non-root USER, install order, dependency pinning, secrets handling, healthcheck), state whether the agent got it right or wrong, and the exact edit you would make. This is the project’s Hard tier in miniature — the judgment is yours, not the agent’s.
Rep 10 — Container vs VM, In Your Own Words
Open the Container vs VM Explorer below the chapter. Toggle between VM and container modes and record the four numbers (boot time, image size, density, isolation) for each.
Write: In three sentences, explain why the container boots faster and isolates more weakly than the VM, using the words “shared kernel,” “namespaces,” and “cgroups.” Name one workload you would still put in a VM rather than a container, and why.
Done? One Last Thing.
Without looking anything up, answer these in writing, two or three sentences each:
- A container and a VM both isolate workloads. Name the one architectural difference between them, and name the two kernel features that make a container’s isolation work.
- You pull a 4 GB model into a containerized Ollama,
docker rmthe container, and the model is gone. What did you forget, and what is the one-line fix? - An agent hands you a Dockerfile starting
FROM python:latestwith noUSERline andENV API_KEY=sk-live-.... Name all three problems and the fix for each. - Why does the OpenAI-compatible
/v1API let the same client code run against Ollama, vLLM, and NIM unchanged — and why is that a “bet on the standard” win?
If you can answer all four cold — the container/VM seam, model persistence, Dockerfile review, and the universal API — you have the chapter. Question 3 is exactly what the project’s Hard tier makes you do for real, with a memo attached.
Up next: Project 7 — Project 7: Ship a Model in a Box.