fix(M7-27+capstone): apply AI-drives-git reframe, lesson=theory, de-slop course-wide

Phase 2 sweep — all modules are post-pivot, so the learner directs the AI agent
(Claude Code as the worked example) to do the git/setup work and verifies, instead
of typing commands by hand; no re-teaching basics. Lesson sections are theory with
example output; all execution lives in the labs. De-slopped ("prose" etc. gone
course-wide, em-dash density thinned). /path/to placeholders -> ~/ai-workflow-course.

Every deliberate teaching device verified intact: M10 ai-change.patch trap,
M12 bad-clear-snippet, M13/M27 planted pending_count bug, M15 secret+typosquat+MD5,
M18 BREAK=1, M21 absent-.gitignore, M22 poisoned skill, M24 no-op patch, M25 --simulate.
Labs compile/parse (py/sh/yaml/json); no junk.

Closes #83
Closes #86
Closes #89

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfzV5QvtPDz8LJS3Pu5VLT
This commit is contained in:
2026-06-22 21:58:17 -04:00
parent a29823f4b3
commit f925fd9645
38 changed files with 1735 additions and 1424 deletions
@@ -1,8 +1,8 @@
# Module 16 — Containers and Reproducible Environments
> **"Works on my machine" is a confession, not a defense.** A container ships the machine with the
> code, so your app, your CI, and your deploy target all run the exact same environment — and gives
> you a throwaway box to run an agent you don't fully trust.
> code, so your app, your CI, and your deploy target all run the exact same environment. It also
> gives you a throwaway box to run an agent you don't fully trust.
---
@@ -15,9 +15,9 @@
module is what makes that clean machine *identical* to your laptop and to where you'll deploy.
- **Module 15** — security scanning and dependency hygiene. Important here as a boundary: a
container faithfully reproduces your dependencies, including the vulnerable ones. Containers are
**not** a substitute for the hygiene Module 15 taught they're downstream of it.
**not** a substitute for the hygiene Module 15 taught; they're downstream of it.
You do **not** need Docker installed yet that's the first step of the lab. This module looks
You do **not** need Docker installed yet; that's the first step of the lab. This module looks
forward to Module 18 (deployment: a container is *what* you ship) and, lightly, to Units 45, where
that same throwaway box becomes the place you let an agent run.
@@ -49,8 +49,8 @@ written down."
Hand the code to a colleague, a CI runner (Module 14), or a server, and the invisible stack is
different. The failures are maddeningly specific: a different Python patch version changes a default,
a system library is missing, an env var you set six months ago and forgot is load-bearing. The bug
isn't in the code. The bug is that the *environment* never traveled with it.
a system library is missing, an env var you set six months ago and forgot turns out to be required.
The bug isn't in the code. The bug is that the *environment* never traveled with it.
A container is the fix: it packages the code **and the invisible stack together** into one artifact
that runs the same everywhere. You stop shipping just the code and start shipping the machine.
@@ -67,7 +67,7 @@ distinction:
- **Registry** — where images are stored and shared, the way a Git remote (Module 8) stores repos.
You `push` an image to a registry and `pull` it elsewhere. (Most git hosts now bundle one.)
- **Dockerfile** — the plain-text recipe that *builds* an image. This is the part you version. It is
the executable, reviewable specification of the environment the same instinct as committing the
the executable, reviewable specification of the environment, the same instinct as committing the
AI's config in Module 5, applied to the whole machine.
### It is not a virtual machine
@@ -78,7 +78,7 @@ and isolates only the process and its filesystem view. It's much closer to a sou
or a BSD jail with packaging and distribution bolted on than to a hypervisor. That's why containers
start in milliseconds and weigh megabytes instead of gigabytes.
Hold onto "shares the host kernel" — it's also exactly why a container is not a strong security
Hold onto "shares the host kernel." It's also exactly why a container is not a strong security
boundary by default (more in *Where it breaks*).
### The Dockerfile, line by line
@@ -101,7 +101,7 @@ Each instruction adds a **layer**. Layers are cached and reused: change only `cl
rebuilds from the `COPY` step down, reusing the base image and everything above. Order your
Dockerfile cheapest-to-most-volatile (base and dependencies first, your fast-changing code last) and
rebuilds stay fast. This is the same reason you install dependencies *before* copying source in a
real project so a one-line code change doesn't reinstall the world.
real project, so a one-line code change doesn't reinstall the world.
### The levers that make it actually reproducible
@@ -114,24 +114,24 @@ levers that close that gap:
`FROM python:3.12-slim@sha256:…`. Choose your point on the spectrum deliberately — a moving tag
picks up security patches automatically; a pinned digest never changes under you. Both are valid;
silence is not.
- **Pin your dependencies.** This is Module 15's lesson, now load-bearing. A Dockerfile that runs
`pip install <pkg>` with no version reproduces *whatever was newest at build time* — which is not
reproducible at all. Use a lockfile. The container is only as deterministic as what you install
into it.
- **Pin your dependencies.** This is Module 15's lesson, and the container is where it bites. A
Dockerfile that runs `pip install <pkg>` with no version reproduces *whatever was newest at build
time*, which is not reproducible at all. Use a lockfile. The container is only as deterministic as
what you install into it.
- **Use a `.dockerignore`.** See [`lab/dockerignore-starter`](lab/dockerignore-starter). What isn't
copied into the build can't bloat the image or leak into it the same instinct as `.gitignore`
copied into the build can't bloat the image or leak into it, the same instinct as `.gitignore`
from Module 2.
### Why this snaps CI and deploy into one line
Module 14 sold CI as "a clean machine that runs your checks." The unsolved half was that the clean
machine still wasn't *your* machine "passes locally, fails in CI" was a real, common, miserable
bug. Containers dissolve it. When CI builds and runs the same image you build and run locally, the
machine still wasn't *your* machine: "passes locally, fails in CI" was a real, common, miserable
bug. Containers remove it. When CI builds and runs the same image you build and run locally, the
environment is identical by construction. "Works in CI but not locally" stops being possible because
there's only one environment now, not two that drift.
The same artifact carries forward: the image CI builds is the image Module 18 deploys. Build once,
run identically laptop, pipeline, production.
run identically on laptop, pipeline, and production.
---
@@ -141,12 +141,12 @@ Docker itself you may already know. What makes containers matter *more* in AI-as
- **AI writes code for an environment it can't see.** The model assumes packages are installed, a
certain runtime version, paths that exist on *its* imagined machine. "Works on my machine"
becomes "works on the machine the model pictured" and that machine is no one's. A Dockerfile
becomes "works on the machine the model pictured," and that machine is no one's. A Dockerfile
forces the environment to be explicit, so the AI's assumptions either hold or fail loudly at build
time instead of mysteriously at run time.
- **The environment becomes reviewable.** AI-suggested setup ("just run these eight commands") drifts
and rots and lives in a chat log. A Dockerfile turns that into one committed, diffable file. When
the AI changes how the environment is built, it arrives as a diff in a PR (Module 10) the same
the AI changes how the environment is built, it arrives as a diff in a PR (Module 10), the same
win as committing the AI's config in Module 5, extended to the whole machine.
- **A container is a sandbox for an agent you don't fully trust.** This is the forward-looking one.
As you let AI do bolder things — run commands, install packages, execute its own code, and
@@ -155,7 +155,7 @@ Docker itself you may already know. What makes containers matter *more* in AI-as
worst, then `docker rm` the whole thing. The host never saw it. This is the practical foundation
for running less-trusted agents, and we'll build on it when MCP servers and skills (Unit 4) start
executing third-party code.
- **But a container does not make AI code safe.** It reproduces whatever the AI wrote including a
- **But a container does not make AI code safe.** It reproduces whatever the AI wrote, including a
hallucinated dependency (Module 15) or a hardcoded secret (Module 17), now faithfully baked into an
image and shipped everywhere. Containers are a *reproducibility and blast-radius* tool, not a
correctness or security tool. They sit alongside Module 15, not on top of it.
@@ -179,13 +179,16 @@ containerize and run the app you already have.
is up with `docker info` (or `podman info`), which only succeeds when the engine is actually live.
- The starter files from this module's `lab/`: [`Dockerfile`](lab/Dockerfile) and
[`dockerignore-starter`](lab/dockerignore-starter).
- Your AI assistant.
- Your coding agent (Claude Code is the worked example; sub your own).
### Part A — Build the image
1. Copy this module's `lab/Dockerfile` into your `tasks-app` folder, and copy
`lab/dockerignore-starter` to a file named exactly `.dockerignore` in the same folder. Read the
Dockerfile top to bottom — every line is commented. Then build:
1. Get the two starter files into your `tasks-app` folder. Direct your agent (Claude Code is the
worked example; sub your own) to do the placement: *"Copy this module's lab/Dockerfile into
`~/ai-workflow-course/tasks-app`, and create a file named exactly `.dockerignore` there from
lab/dockerignore-starter."* Then read the Dockerfile top to bottom yourself before you build:
every line is commented, and you want to know what you're about to run, not just that the file
landed. The build is the lesson, so you run it by hand:
```bash
cd ~/ai-workflow-course/tasks-app
@@ -253,9 +256,10 @@ containerize and run the app you already have.
### Part D — Use the container as a sandbox (the AI angle, hands-on)
4. Now use a disposable container as a blast-radius box for something you don't fully trust. Ask your
AI for a one-line shell command that "inspects the system" — the kind of thing you'd hesitate to
paste straight into your real terminal. Then run it where it can't touch your host: no network,
read-only root filesystem, and nothing of yours mounted:
agent (Claude Code is the worked example; sub your own) for a one-line shell command that
"inspects the system," the kind of thing you'd hesitate to paste straight into your real terminal.
Then run it where it can't touch your host: no network, read-only root filesystem, and nothing of
yours mounted:
```bash
docker run --rm --network none --read-only python:3.12-slim \
@@ -265,16 +269,19 @@ containerize and run the app you already have.
`--network none` cuts it off from the internet; `--read-only` stops it writing to the container
filesystem; `--rm` destroys the container after. Whatever the command does, it does it to a box
that exists for one second and touches nothing you care about. **This is the pattern** for running
less-trusted commands and, later, less-trusted agents the foundation Units 45 build on. (Read
less-trusted commands and, later, less-trusted agents: the foundation Units 45 build on. (Read
*Where it breaks* before you trust it with something genuinely hostile.)
5. Commit your work. The Dockerfile and `.dockerignore` are environment-as-code version them like
anything else:
5. Commit your work. The Dockerfile and `.dockerignore` are environment-as-code, so version them
like anything else. Direct your agent (Claude Code is the worked example; sub your own) to stage
and commit them: *"Stage the Dockerfile and .dockerignore and commit them with a clear message
about containerizing the tasks-app for a reproducible environment."*
```bash
git add Dockerfile .dockerignore
git commit -m "Containerize the tasks-app for a reproducible environment"
```
Then verify the result, because what got committed is the point. Have the agent show you the
commit (`git show --stat HEAD`) and confirm it staged **only** those two files. `tasks.json`
should be absent: your `.dockerignore` and `.gitignore` exclude it, and runtime state has no
business in either the image or the repo. If the agent staged anything you didn't expect, that's
the review gate (Module 10) doing its job before the environment-as-code ships.
---
@@ -290,13 +297,13 @@ Be honest about the limits — this audience will find them the hard way otherwi
capabilities, seccomp/AppArmor profiles, and for genuinely hostile workloads a stronger sandbox
with its own kernel (gVisor, Kata Containers, or a real VM). Treat the lab's `--network none
--read-only` as raising the cost of mischief, not as a guarantee against a determined attacker.
- **Reproducible ≠ small.** A naive image can be hundreds of megabytes to multiple gigabytes
- **Reproducible ≠ small.** A naive image can be hundreds of megabytes to multiple gigabytes:
full base images, build toolchains left in the final layer, the `.git` directory copied in.
Bloat is slow to pull, expensive to store, and a larger attack surface. The defenses: slim or
distroless base images, multi-stage builds (build in a fat image, copy only the artifact into a
thin one), and a real `.dockerignore`.
- **It does not replace dependency hygiene (Module 15).** A container reproduces your dependencies
*perfectly* including the vulnerable and the hallucinated ones. Pinning a base image with a known
*perfectly*, including the vulnerable and the hallucinated ones. Pinning a base image with a known
CVE just reproduces that CVE on every machine, reliably. Containers are downstream of Module 15,
not a substitute: you still scan dependencies, and you scan the *image itself* (its base layers
carry their own vulnerabilities).
@@ -327,7 +334,7 @@ Be honest about the limits — this audience will find them the hard way otherwi
why the host was safe — *and* can name one case where it wouldn't have been.
- You can state, without looking back: a container is not a VM, it's not a security boundary by
default, and it doesn't replace dependency hygiene from Module 15.
- Your `Dockerfile` and `.dockerignore` are committed the environment is now version-controlled,
- Your `Dockerfile` and `.dockerignore` are committed: the environment is now version-controlled,
reviewable config.
When "works on my machine" stops being something you say and starts being something you build, you're