feat(root04): author Containers & Docker

Adds all 8 ROOT 04 modules (why containers exist, images & containers, Docker CLI, container
networking, container storage, Dockerfile & image build, Docker Compose, containers →
Kubernetes) — 43 activities, using the nested Docker Engine Phase 6D built and tested. Modules
1, 2, and 8 are conceptual (no lab by design) — the honest bridge module (M8) explicitly names
what Kubernetes adds beyond what this course already taught, without re-teaching Kubernetes
itself.

Adds 5 new hands-on Docker scenarios plus 2 MCQs, all against the real nested Docker Engine:
docker-cli-basics (run/exec/inspect a real container), docker-container-networking (a genuine
"works internally, unreachable from host" port-publishing failure, diagnosed and fixed by
recreating the container with the correct `-p` flag), docker-container-storage (a real
destroy/recreate cycle proving named-volume persistence, not just described), docker-build-image
(a real Dockerfile, a real build, a real custom image run), and docker-compose-multiservice (a
real two-service Compose application with a real published port). All tested end-to-end via the
real scenario-validation API, including negative tests.

Wires rootnode-containers-assessment (2 required + 4 random-draw scenarios) using the existing
Phase 3B assessment engine.

This completes the entire ROOT_ Foundations sequence (ROOT 01-04) — see
docs/PHASE6E_ROOT_FOUNDATIONS_COMPLETION.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-08-26 17:08:14 -05:00
co-authored by Claude Sonnet 5
parent e68b6f445a
commit 682ef2de47
45 changed files with 1970 additions and 0 deletions
@@ -0,0 +1,58 @@
# Container Networking
## This module is ROOT 02's networking model, one layer up
Every concept here is a direct application of ROOT 02 Module 3's host-to-application chain —
just with a container standing in for "host."
## Every container gets its own IP
By default, Docker gives each container its own IP address on a private, internal Docker
network — exactly like a real host has an IP on a real network (ROOT 02 Module 3). Containers on
the SAME Docker network can reach each other directly by that IP.
## Container-to-container DNS: reach a container by NAME
Docker runs its own internal DNS for user-defined networks — containers on the same network can
resolve each other by CONTAINER NAME, not just IP. If a `web` container needs to reach an `api`
container, it can simply connect to hostname `api` — Docker resolves it automatically. This is
the exact "getent hosts" concept from ROOT 02, just automated for you.
## Publishing a port: the host/container boundary
A container's ports are, by default, only reachable from OTHER containers on the same Docker
network — NOT from the host machine, and NOT from outside. **Publishing** a port
(`-p hostPort:containerPort`) explicitly creates a mapping: traffic arriving at the HOST's
`hostPort` gets forwarded to the CONTAINER's `containerPort`. This is set at container CREATION
time — you cannot add it to an already-running container; it must be recreated.
```
docker run -d -p 8080:80 nginx:alpine
```
Host port 8080 → this container's port 80. `curl http://localhost:8080/` from the host now
reaches it; `curl http://localhost:80/` from the host does NOT (the host's own port 80 was never
touched).
## A working app that's still unreachable is a real, common failure
Exactly like ROOT 02's networking-diagnosis modules: a container can be perfectly healthy
internally (`docker exec <container> curl localhost:80` succeeds) while being completely
unreachable from the host, because publishing was never configured. This is one of the single
most common container networking mistakes — and this module's Lab makes you diagnose and fix
it for real.
## Instructor Notes
**Lesson goal:** Explicitly connect every idea here back to ROOT 02's networking model — this
module should feel like recognition, not new material, reinforcing the curriculum's continuity.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** assuming a container's port is automatically reachable
from the host just because the container itself works fine — this exact misconception is what
this module's Lab is built to correct through direct experience.
**Discussion question:** "Why might Docker default to NOT exposing container ports to the host
automatically?" (Good answer: explicit opt-in is safer — you don't want every container's every
port accidentally reachable from outside by default.)
@@ -0,0 +1,51 @@
# Demonstration: Reaching a Container By Name, Not IP
## OBSERVE
```
$ docker network create demo-net
$ docker run -d --name api-server --network demo-net nginx:alpine
```
## HYPOTHESIS
Predict: can another container on `demo-net` reach `api-server` using its NAME as a hostname,
without knowing its IP?
## INSPECT
```
$ docker run --rm --network demo-net alpine:3.19 sh -c "wget -qO- http://api-server:80/ | head -1"
<!DOCTYPE html>
```
Yes — Docker's built-in DNS resolved `api-server` (a container NAME) directly, no IP address
needed anywhere in that command.
## CHANGE
Now try reaching it from a container that is NOT on the same network:
```
$ docker run --rm alpine:3.19 sh -c "wget -qO- http://api-server:80/ 2>&1 | head -1"
wget: bad address 'api-server'
```
Name resolution fails entirely — the container isn't even on the same Docker network, so there's
no DNS entry for it there.
## VERIFY
Container-to-container name resolution is scoped to a SPECIFIC Docker network, not global —
exactly like ROOT 02's subnet concept: reachability depends on which network you're actually on.
## Instructor Notes
**Demo goal:** Show container-name DNS resolution as a real, working feature — and show it
explicitly FAILING when containers aren't on the same network, reinforcing that this isn't
magic, it's scoped.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** assuming any two containers can always reach each other by name
regardless of network — reinforce the "same network required" rule explicitly.
@@ -0,0 +1,40 @@
# Knowledge Check: Container Networking
---
**1. A container works fine when tested with `docker exec <container> curl localhost:80`, but
`curl http://localhost:8080/` from the host fails. What's the most likely cause?**
*Answer:* The container was never started with a port-publish flag (`-p 8080:80`) mapping the
host port to the container's port — the app works internally, but nothing connects the host to
it.
---
**2. Can you add port publishing to an already-running container?**
*Answer:* No — port publishing is set at container creation time; you must remove and recreate
the container with the correct `-p` flag.
---
**3. How can one container reach another by name instead of IP?**
*Answer:* If both containers are on the same user-defined Docker network, Docker's built-in DNS
resolves the target container's NAME automatically — no IP address needed.
---
**4. Why does container-name resolution fail between two containers that AREN'T on the same
Docker network?**
*Answer:* Docker's per-network DNS only knows about containers actually attached to that
specific network — being on a different (or no) network means there's no DNS entry for the
target container at all.
## Instructor Notes
**Facilitation:** Question 1 directly previews this module's Lab — a good moment to build
anticipation for it.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,29 @@
# Guided Exercise: Diagnose Before You Recreate Anything
This is a paper exercise — commit to a diagnostic plan before working in the Lab.
## Your task
A container `payments-api` is reported as "unreachable" by a teammate. Before touching anything,
write out, in order, the exact commands you'd run to determine WHERE the problem actually is —
using this module's and ROOT 02's tools.
## A worked shape to check yourself against
```
docker ps --filter name=payments-api # is it even running?
docker exec payments-api curl -sf localhost:PORT/ # does it work internally?
docker port payments-api # is anything actually published?
curl -sf http://localhost:PUBLISHED_PORT/ # does the published mapping actually work?
```
This sequence isolates each layer independently — running? working internally? published at
all? published mapping actually functional? — exactly the same "isolate one variable at a time"
discipline ROOT 02 Module 3 taught, applied here to containers specifically.
## Instructor Notes
**Facilitation:** This exercise is deliberately a direct copy of ROOT 02's diagnostic discipline
— the value is students recognizing that connection themselves, not being told it explicitly.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,25 @@
# Reflection: Same Diagnosis, New Layer
## Reflection questions
1. How much of this module's Lab did you solve using pure Docker knowledge, versus reasoning
you already had from ROOT 02?
2. Where do you expect "published port" reasoning to matter again later in ROOT 05+?
## Real Ops takeaway
**"Works internally, unreachable externally" is one of the single most common real-world
container networking incidents — and you just diagnosed and fixed a real one.** The exact
reasoning you used here (confirm internal health, check what's actually published, verify the
published mapping works) is the same reasoning you'll use again for Kubernetes Services later —
"published port" becomes "Service," but the underlying question (is anything actually routing
traffic to this workload?) doesn't change.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 previews Kubernetes Services directly
— worth naming that connection explicitly without teaching Services yet.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.