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,51 @@
# Why Containers Exist
## "It works on my machine"
Every engineer has heard this sentence, and it names a real, expensive problem: an application
that runs perfectly on one machine fails on another because of differences in installed
libraries, OS versions, configuration, or dozens of other environmental details that are easy to
overlook and hard to fully document.
## The problem, precisely
- **Dependency inconsistency** — an app needs Python 3.11 and a specific set of libraries; the
production server has Python 3.9 and different library versions.
- **Non-repeatability** — setting up a working environment by hand, following a README, drifts
over time and between people — "works on my machine" is often true because "my machine" has
accumulated years of undocumented manual setup.
- **Deployment friction** — moving an application from a developer's laptop to a test server to
production means repeating fragile manual setup multiple times, each an opportunity for drift.
## Containers: package the application AND everything it needs, together
A **container** packages an application with its exact dependencies, libraries, and
configuration into one deployable unit — the same container that ran on a developer's laptop
runs, byte-for-byte identically, on a test server or in production. "It works on my machine"
becomes "it works in this container," which is true everywhere that container runs.
## VM vs. container — genuinely different, not just "containers are lighter VMs"
A **virtual machine** virtualizes an entire computer, including its own full operating system
kernel — heavier, slower to start, but with very strong isolation. A **container** shares the
HOST machine's kernel and only isolates the application's process, filesystem view, and network
— dramatically lighter and faster to start (seconds vs. minutes), with somewhat different
isolation properties than a full VM (a well-understood, documented tradeoff, not a hidden flaw).
Neither is strictly "better" — they solve overlapping but distinct problems, and real production
systems often use both together (VMs providing strong host isolation, containers providing fast,
consistent application packaging on top).
## Instructor Notes
**Lesson goal:** Motivate containers through the real, felt problem ("it works on my machine")
before any Docker command — mirrors ROOT 03's "why version control exists" opener.
**Approximate timing:** 12 minutes read; 8 minutes live.
**Common misconception to watch for:** describing containers as simply "lightweight VMs" —
they share the host kernel, which is a genuine architectural difference, not just a size
difference.
**Discussion question:** "Has anyone experienced 'it works on my machine' personally, on either
side of that conversation?" (Real stories make this lesson's premise concrete before the
technical content begins.)
@@ -0,0 +1,49 @@
# Demonstration: What "Shares the Host Kernel" Actually Looks Like
## OBSERVE
```
$ uname -r
5.15.0-real-host-kernel
$ docker run --rm alpine:3.19 uname -r
5.15.0-real-host-kernel
```
## HYPOTHESIS
Predict: would a real virtual machine show the SAME kernel version as its host, the way this
container just did?
## INSPECT
A real VM runs its OWN independent kernel — it would very likely show a DIFFERENT version (or
even a different OS entirely) than its host, because a VM virtualizes the hardware and boots a
complete, separate operating system on top of it.
## CHANGE
```
$ docker run --rm ubuntu:22.04 uname -r
5.15.0-real-host-kernel
```
Even a completely different container OS (`ubuntu` here, vs. `alpine` before) STILL reports the
exact same kernel version — because there's only ONE real kernel involved, the host's.
## VERIFY
This is the concrete, observable meaning of "containers share the host kernel": no matter what
container image you run, `uname -r` always reports the same real kernel underneath — this is
exactly why containers start in a fraction of a second (no separate kernel to boot) and exactly
the tradeoff the lesson named.
## Instructor Notes
**Demo goal:** Make "shares the host kernel" a concrete, checkable fact rather than an abstract
architectural claim.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** assuming different container images run different kernels
because their USERSPACE (installed tools, package manager) looks completely different — the
kernel underneath is unified regardless.
@@ -0,0 +1,33 @@
# Knowledge Check: Why Containers Exist
---
**1. What real problem does "it works on my machine" name?**
*Answer:* Dependency/environment inconsistency between where an application was developed and
where it actually needs to run — differences in libraries, versions, and configuration that are
easy to overlook and hard to fully document by hand.
---
**2. What's the actual architectural difference between a container and a virtual machine, not
just "containers are smaller"?**
*Answer:* A container shares the host machine's kernel and only isolates the application's
process/filesystem/network view. A VM virtualizes an entire computer, including running its own
separate kernel — a genuinely different isolation model, not just a size difference.
---
**3. Why do containers start so much faster than VMs?**
*Answer:* A container doesn't boot a separate operating system/kernel — it starts a process
using the ALREADY-RUNNING host kernel, while a VM has to boot an entire independent OS from
scratch.
## Instructor Notes
**Facilitation:** Question 2 is the one worth confirming explicitly — many students carry the
misconception "container = tiny VM" past this point without correction.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,26 @@
# Reflection: You Now Know What Problem You're About to Solve, Hands-On
## Reflection questions
1. Think back to ROOT 01's "how computers run software" module. How does today's lesson refine
your understanding of what a "process" actually is?
2. Do you expect containers to make the Linux/process/networking skills from ROOT 01-02 less
relevant, or MORE relevant? Why?
## Real Ops takeaway
**Every skill from ROOT 01-02 — processes, filesystems, networking — transfers directly into
containers, because a container is built from exactly those same primitives, just isolated
differently.** You are not about to learn an unrelated new subject; you're about to see the
Linux fundamentals you already have applied through Docker's specific tooling. Module 2 starts
that translation.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2's honest answer is "more relevant" —
worth stating explicitly, since students sometimes assume containers are an alternative to
Linux fundamentals rather than built directly on top of them.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,51 @@
# Images and Containers
## The single most important distinction in this course
**An image is a packaged, read-only ARTIFACT. A container is a RUNNING INSTANCE of an image.**
This is exactly analogous to a program's executable file (the image) versus a running process
of that program (the container) — a distinction ROOT 01's process module already gave you
vocabulary for. You can run the SAME image many times, producing many independent containers,
the same way you can run the same program multiple times as separate processes.
## Layers: how an image is actually built
An image is built from a stack of **layers**, each representing one change (install a package,
copy in application code, set a configuration). Layers are cached and shared — if two images
both start `FROM alpine:3.19`, they share that base layer on disk rather than duplicating it,
which is why pulling a new image that shares layers with one you already have is often much
faster than the total image size suggests.
## Registries, repositories, and tags
- **Registry** — a server that stores and distributes images (Docker Hub is the default public
one; a company might run a private registry).
- **Repository** — a named collection of related images (e.g. `nginx`).
- **Tag** — a specific version within a repository (`nginx:alpine`, `nginx:1.25`, `nginx:latest`).
`latest` is just a conventional tag name, not automatically "the newest" in any enforced sense
— relying on it in production is a common, avoidable source of surprise.
## The core commands, mapped to the model
- **`docker pull <image>`** — download an image from a registry, without running it.
- **`docker run <image>`** — create and start a NEW container from an image (pulls automatically
if not already present locally).
- **`docker stop <container>`** — gracefully stop a running container (its process, not the
image).
- **`docker rm <container>`** — delete a stopped container. This does NOT delete the image it
was created from.
## Instructor Notes
**Lesson goal:** Land the image/container distinction as concretely as possible — nearly every
later confusion in this course traces back to conflating the two.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** using "image" and "container" interchangeably in casual
speech — gently correct this early and often, since precise vocabulary here prevents real
confusion later (e.g., "did you delete the container or the image?" matters).
**Discussion question:** "If you run `docker run nginx:alpine` three times, how many images
exist afterward? How many containers?" (Answer: 1 image, 3 containers — a good check before
moving to the hands-on Lab.)
@@ -0,0 +1,59 @@
# Demonstration: One Image, Many Independent Containers
## OBSERVE
```
$ docker images | grep alpine
alpine 3.19 a1b2c3d4e5f6 2 weeks ago 7.4MB
```
Exactly one `alpine:3.19` image exists locally right now.
## HYPOTHESIS
Predict: after running three containers from it, how many images will `docker images` show?
## INSPECT
```
$ docker run -d --name c1 alpine:3.19 sleep 300
$ docker run -d --name c2 alpine:3.19 sleep 300
$ docker run -d --name c3 alpine:3.19 sleep 300
$ docker images | grep alpine
alpine 3.19 a1b2c3d4e5f6 2 weeks ago 7.4MB
```
Still exactly one image — confirmed.
## CHANGE
```
$ docker ps --format '{{.Names}}: {{.ID}}'
c3: 9f8e7d6c5b4a
c2: 3d2c1b0a9f8e
c1: 7c6b5a4d3e2f
```
Three DIFFERENT containers, each with its own ID, all created from that same one image.
## VERIFY
```
$ docker rm -f c1 c2 c3
$ docker images | grep alpine
alpine 3.19 a1b2c3d4e5f6 2 weeks ago 7.4MB
```
Removing all three containers left the image completely untouched — proving containers and the
image they came from have independent lifecycles.
## Instructor Notes
**Demo goal:** Make the 1-image-many-containers relationship undeniable by actually counting
both before and after, rather than just stating it.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** assuming `docker rm` on a container also removes its image —
a good moment to reinforce that images and containers have separate removal commands
(`docker rm` for containers, `docker rmi` for images) for exactly this reason.
@@ -0,0 +1,38 @@
# Knowledge Check: Images and Containers
---
**1. State the image/container distinction in one sentence.**
*Answer:* An image is a packaged, read-only artifact; a container is a running instance created
from an image — the same relationship as a program file and a running process of it.
---
**2. You run `docker rm -f my-container`. Does this delete the image it was created from?**
*Answer:* No — removing a container never removes the image; they have independent lifecycles
and separate removal commands (`docker rm` vs. `docker rmi`).
---
**3. What does the `latest` tag actually guarantee?**
*Answer:* Nothing enforced — it's just a conventional tag name, not automatically "the newest
version" in any technical sense. Relying on it in production is a common source of surprise.
---
**4. What's a layer, and why does layer sharing matter practically?**
*Answer:* A layer is one recorded change in an image's build (installing something, copying
files, etc.). Layers are cached and shared between images that share a base — meaning pulling a
new image that shares layers with one you already have can be much faster than its total
reported size suggests.
## Instructor Notes
**Facilitation:** Question 2 directly reinforces the previous demo — worth confirming everyone
got it right before moving on.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,31 @@
# Guided Exercise: Trace the Counts Through a Sequence of Commands
This is a paper exercise — commit to your answer before checking it in the Lab.
## Your task
Starting from a completely clean system (no images, no containers), predict the number of
IMAGES and the number of CONTAINERS after each step:
1. `docker pull redis:alpine`
2. `docker run -d --name cache1 redis:alpine`
3. `docker run -d --name cache2 redis:alpine`
4. `docker rm -f cache1`
5. `docker rmi redis:alpine` (attempted while `cache2` is still running)
## A worked shape to check yourself against
1. 1 image, 0 containers.
2. 1 image, 1 container.
3. 1 image, 2 containers.
4. 1 image, 1 container (only `cache1` removed).
5. This actually FAILS — Docker refuses to remove an image that a running container (`cache2`)
still depends on, specifically to prevent exactly this kind of accident. Still 1 image, 1
container, plus an error message.
## Instructor Notes
**Facilitation:** Item 5 is the most valuable one — many students haven't seen Docker's
dependency-protection behavior yet, and it's a good real safety feature to highlight.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,24 @@
# Reflection: A Small Distinction With Large Consequences
## Reflection questions
1. Before this module, would you have known why `docker rm` and `docker rmi` are two separate
commands? Does it make sense now?
2. Where else in ROOT_ have you seen this exact "artifact vs. running instance" pattern before?
(Hint: think back to ROOT 01.)
## Real Ops takeaway
**Precise vocabulary prevents real mistakes.** Confusing "stop the container" with "delete the
image," or assuming `latest` means "the current version," are genuine, common sources of
production incidents — not just semantic nitpicking. The image/container distinction you just
solidified is the foundation every command in the rest of this course builds on.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2's intended answer is "program vs.
process," from ROOT 01 — a good moment to explicitly connect the two courses.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,67 @@
# Docker CLI
## The commands you'll use constantly
Module 2 introduced pull/run/stop/rm conceptually. This module adds the rest of the everyday
toolkit — and every one of them maps directly to a Linux concept you already have from ROOT 01.
## `docker ps` — what's running right now
```
$ docker ps
CONTAINER ID IMAGE STATUS NAMES
a1b2c3d4e5f6 nginx:alpine Up 2 minutes web
```
This is the container-level equivalent of ROOT 02's `ps aux` — "what's actually running." Add
`-a` to see stopped containers too, not just running ones.
## `docker logs` — what has it been saying?
```
$ docker logs web
```
The container-level equivalent of reading a service's log file from ROOT 02 — except Docker
captures a container's stdout/stderr automatically, so there's no config needed to find WHERE
the logs live, unlike a traditional service.
## `docker exec` — run a NEW command inside an ALREADY-running container
```
$ docker exec web sh -c 'cat /etc/nginx/nginx.conf'
```
Different from `docker run`, which creates a brand-new container — `exec` reaches INTO an
existing, running one, exactly like ROOT 01's `ps`/inspection commands but scoped to one
specific container's process namespace.
## `docker inspect` — the full, detailed truth about a container
```
$ docker inspect web --format '{{.State.Running}}'
true
```
Returns a large JSON document with everything Docker knows about a container — `--format` lets
you extract just the one field you care about, rather than reading the whole thing.
## `docker stop` vs `docker rm`
`docker stop` gracefully stops a running container (sends SIGTERM, then SIGKILL after a grace
period — literally ROOT 01's process-signals lesson, applied to a container's main process).
`docker rm` deletes a STOPPED container entirely — `docker rm -f` combines both (force-stop then
remove) in one step.
## Instructor Notes
**Lesson goal:** Explicitly map every Docker CLI command back to the equivalent ROOT 01/02
Linux concept students already have — this is a translation exercise, not new material.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** confusing `docker run` (creates a new container) with
`docker exec` (runs a command in an existing one) — a very common early mixup.
**Demo idea:** run `docker exec` against a container, then immediately `docker run` the same
image again, and compare `docker ps` before/after to show the difference concretely.
@@ -0,0 +1,62 @@
# Demonstration: `run` Creates, `exec` Reaches In
## OBSERVE
```
$ docker run -d --name web nginx:alpine
$ docker ps --format '{{.Names}}'
web
```
One container exists.
## HYPOTHESIS
Predict: will `docker exec web echo hello` create a SECOND container?
## INSPECT
```
$ docker exec web echo hello
hello
$ docker ps --format '{{.Names}}'
web
```
Still exactly one container — `exec` ran a new command INSIDE the existing one, it never
created anything new.
## CHANGE
Now actually run the same image again:
```
$ docker run -d --name web2 nginx:alpine
$ docker ps --format '{{.Names}}'
web2
web
```
Now two containers exist — because `run`, unlike `exec`, always creates a NEW container.
## VERIFY
```
$ docker logs web | tail -3
$ docker inspect web --format '{{.State.Running}}'
true
```
Both containers are independent, each with their own logs and state — exactly as Module 2's
"one image, many containers" demonstration showed.
## Instructor Notes
**Demo goal:** Directly contrast `run` and `exec`'s effect on the container COUNT — this is the
most common early confusion and deserves a side-by-side comparison.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** trying to `exec` into a container that isn't running yet
(`exec` requires an ALREADY-running target) — a good moment to connect back to `docker ps`
first.
@@ -0,0 +1,39 @@
# Knowledge Check: Docker CLI
---
**1. What's the real difference between `docker run` and `docker exec`?**
*Answer:* `docker run` always creates a brand-new container from an image. `docker exec` runs a
new command inside an ALREADY-running container — no new container is created.
---
**2. Which command is the container-level equivalent of ROOT 02's "what's running" question
(`ps aux` / service inspection)?**
*Answer:* `docker ps` (add `-a` to include stopped containers too).
---
**3. Why doesn't `docker logs` require you to know a specific log file path, unlike a
traditional Linux service?**
*Answer:* Docker automatically captures a container's stdout/stderr output — there's no
separate log file configuration needed the way ROOT 02's services required.
---
**4. What does `docker stop` actually send to a container's main process, and why does that
matter?**
*Answer:* SIGTERM first (a polite shutdown request, giving the process a chance to clean up),
then SIGKILL after a grace period if it hasn't exited — the exact same signal discipline ROOT 01
taught for regular Linux processes, applied here to a container's main process.
## Instructor Notes
**Facilitation:** Question 1 is the single most important check in this module — revisit the
demo if students get it wrong.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,29 @@
# Guided Exercise: Which Docker Command Actually Does This?
This is a paper exercise — commit to an answer before checking it in the Lab.
## Your task
For each goal, name the ONE Docker command you'd use:
1. "See every container, including ones that have stopped."
2. "Run a one-off diagnostic command inside a container that's already running."
3. "Read what a container has printed to its logs so far."
4. "Check the exact value of one specific field (like whether it's currently running) without
reading the entire inspect output."
5. "Completely remove a running container in one step, without stopping it first separately."
## A worked shape to check yourself against
1. `docker ps -a`
2. `docker exec <container> <command>`
3. `docker logs <container>`
4. `docker inspect <container> --format '{{...}}'`
5. `docker rm -f <container>`
## Instructor Notes
**Facilitation:** Have students justify why each answer fits the described GOAL specifically,
not just recite familiar syntax.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,24 @@
# Reflection: The Whole Toolkit Was Already Familiar
## Reflection questions
1. Which Docker command in this module felt like "a new version of something I already knew"
from ROOT 01 or ROOT 02? Which felt genuinely new?
2. You ran, inspected, execed into, and removed a real container this module. What's the ONE
command you'd reach for first if a container "seemed broken" and you didn't know why yet?
## Real Ops takeaway
**Every Docker command in this module is a direct translation of a Linux skill you already
had.** `docker ps` is `ps aux`. `docker logs` is reading a log file, minus the config. `docker
exec` is reaching into a running process. This is exactly why ROOT 01-02 came before ROOT 04 —
containers don't replace Linux fundamentals, they wrap them in a specific, consistent interface.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2's honest answer is usually `docker ps`
or `docker logs` — a good moment to connect back to ROOT 02's five-question service checklist.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -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.
@@ -0,0 +1,61 @@
# Container Storage
## The default: a disposable writable layer
Every running container gets its own writable layer sitting on top of its image's read-only
layers. Anything written there — a log file, a temp file, application data — exists ONLY as long
as that specific container does. Destroy the container, and that data is gone permanently. This
is ROOT 02's "persistent vs. temporary" question, given a hard default answer: by default,
everything in a container is temporary.
## Bind mounts: sharing a specific host path
A **bind mount** connects a specific path on the HOST filesystem directly into the container:
```
docker run -v /host/path:/container/path myimage
```
Changes are visible on BOTH sides immediately — useful for development (edit code on the host,
see changes in the container instantly) but tightly coupled to that exact host's filesystem
layout, which makes it a poor choice for portable, production data.
## Named volumes: Docker-managed persistent storage
A **named volume** is storage Docker manages for you, independent of any specific host path:
```
docker volume create my-data
docker run -v my-data:/container/path myimage
```
A named volume's data survives container destruction AND recreation — attaching the SAME volume
to a brand-new container gives that new container access to exactly the same data the old one
left behind. This is the deliberate, production-appropriate way to persist container data.
## The test that actually proves persistence
Claiming "this data persists" isn't convincing without proof: write data, DESTROY the container
completely, create a brand new one attached to the same volume, and confirm the data is still
there. This module's Lab requires exactly that proof, not just a claim.
## Bridge to Kubernetes
A Kubernetes `PersistentVolume`/`PersistentVolumeClaim` is the same idea generalized across an
entire cluster instead of one machine — the exact concept this module teaches, just orchestrated
at a larger scale, which Kubernetes courses build on directly.
## Instructor Notes
**Lesson goal:** Make "containers are disposable, volumes are not" a hard, memorable
distinction — directly building on ROOT 02's persistent/temporary framing.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** assuming ANY container data survives by default — the
default is the OPPOSITE; persistence requires deliberate action (a volume or bind mount).
**Discussion question:** "Why might a named volume be preferred over a bind mount for
production data, even though both technically 'persist' data?" (Answer: a named volume isn't
tied to one specific host's exact filesystem path, making it more portable across different
hosts/environments.)
@@ -0,0 +1,63 @@
# Demonstration: Proving Data Loss, Then Proving Persistence
## OBSERVE
```
$ docker run -d --name no-volume alpine:3.19 sleep 300
$ docker exec no-volume sh -c "echo important-data > /tmp/data.txt"
$ docker exec no-volume cat /tmp/data.txt
important-data
```
Data exists — but is it actually safe?
## HYPOTHESIS
Predict: will `/tmp/data.txt` still exist if this exact container is destroyed and an identical
new one is started?
## INSPECT
```
$ docker rm -f no-volume
$ docker run -d --name no-volume alpine:3.19 sleep 300
$ docker exec no-volume cat /tmp/data.txt
cat: /tmp/data.txt: No such file or directory
```
Gone — proven, not assumed. The "new" container is a completely fresh writable layer, even
though it has the exact same name and image.
## CHANGE
Now with a named volume instead:
```
$ docker rm -f no-volume
$ docker volume create demo-vol
$ docker run -d --name with-volume -v demo-vol:/data alpine:3.19 sleep 300
$ docker exec with-volume sh -c "echo important-data > /data/data.txt"
$ docker rm -f with-volume
$ docker run -d --name with-volume -v demo-vol:/data alpine:3.19 sleep 300
```
## VERIFY
```
$ docker exec with-volume cat /data/data.txt
important-data
```
Proven, not assumed — the exact same destroy-and-recreate sequence, but this time the data
survived, because it lived in the volume, not the container's disposable writable layer.
## Instructor Notes
**Demo goal:** Prove BOTH the loss and the persistence with real destroy/recreate cycles — the
contrast is what makes this lesson land, not either half alone.
**Approximate timing:** 10 minutes live.
**Common mistake to watch for:** stopping (not removing) a container and concluding "data
persists" — `docker stop` doesn't touch the writable layer at all; only `docker rm` (destroying
the container entirely) actually tests real persistence.
@@ -0,0 +1,39 @@
# Knowledge Check: Container Storage
---
**1. By default, what happens to data written inside a running container when that container is
destroyed?**
*Answer:* It's lost permanently — a container's writable layer is disposable by default; nothing
persists unless a volume or bind mount was explicitly configured.
---
**2. What's the actual difference between a bind mount and a named volume?**
*Answer:* A bind mount connects a specific HOST filesystem path directly into the container
(tightly coupled to that host's exact layout). A named volume is storage Docker manages
independently of any specific host path, making it more portable and the production-appropriate
choice.
---
**3. Does `docker stop` (without removing) test whether a container's data actually persists?**
*Answer:* No — stopping doesn't touch the writable layer at all. Only fully removing
(`docker rm`) and recreating the container is a real test of persistence.
---
**4. What Kubernetes concept is this module's volume idea the direct foundation for?**
*Answer:* PersistentVolume/PersistentVolumeClaim — the same persistence concept, generalized
across an entire cluster instead of one machine.
## Instructor Notes
**Facilitation:** Question 3 is worth emphasizing — it's a genuine, common mistake in verifying
persistence claims.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,29 @@
# Guided Exercise: Predict What Survives Before You Test It
This is a paper exercise — commit to predictions before working in the Lab.
## Your task
For each scenario, predict whether the data survives a `docker rm -f` followed by recreating a
container with the same name and image:
1. Data written to `/tmp/file.txt` inside a container with no volumes configured.
2. Data written to `/data/file.txt`, where `/data` is mounted from a named volume.
3. Data written to `/data/file.txt` in scenario 2, but this time you ALSO run
`docker volume rm <the-volume>` before recreating the container.
## A worked shape to check yourself against
1. Lost — no volume, so it lived only in the disposable writable layer.
2. Survives — the volume itself is untouched by the container's destruction, and the new
container reattaches to the same volume.
3. Lost — the volume itself was explicitly deleted, not just the container. Persistence depends
on the VOLUME surviving, not just avoiding container destruction.
## Instructor Notes
**Facilitation:** Item 3 is the important trick question — students who've internalized
"volumes always persist" without qualification should be corrected here: the volume itself can
still be deleted deliberately.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,27 @@
# Reflection: You Proved Persistence, You Didn't Just Trust It
## Reflection questions
1. Before this module, would you have assumed container data persists by default? How
expensive would that wrong assumption be in a real production incident?
2. Why did this module insist on actually DESTROYING a container to test persistence, rather
than just describing the concept?
## Real Ops takeaway
**"I assumed it would persist" is one of the most expensive assumptions in real operations
work.** You didn't just learn that volumes persist data — you personally destroyed a container
and confirmed data loss, then destroyed another and confirmed data survival, with the SAME
sequence of commands each time except for one deliberate volume configuration. That's the level
of confidence "verify, don't assume" is supposed to give you, and it's exactly the discipline
ROOT 02's troubleshooting framework already built.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 works well as a real cautionary
example — ask if anyone has heard of (or experienced) a real data-loss incident caused by this
exact wrong assumption.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,50 @@
# Dockerfile and Image Build
## A Dockerfile is a recipe for an image
Every image you've used so far (`nginx:alpine`, `alpine:3.19`) was itself BUILT from a
Dockerfile by someone else. This module teaches you to write your own.
## The core instructions
- **`FROM <image>`** — every Dockerfile starts by choosing a base image to build on top of.
Almost nothing is built completely from scratch.
- **`WORKDIR <path>`** — sets the working directory for every instruction that follows it (and
for the container's default working directory when it runs).
- **`COPY <host-path> <image-path>`** — copies files from your BUILD CONTEXT (the directory you
run `docker build` from) into the image.
- **`RUN <command>`** — executes a command DURING the build, creating a new layer with its
result (installing a package, for example).
- **`CMD <command>`** (conceptually) — sets the DEFAULT command a container runs when started
from this image, if nothing else is specified.
- **`ENTRYPOINT <command>`** (conceptually) — similar to `CMD`, but harder to override at
`docker run` time — the two are often combined (`ENTRYPOINT` as the fixed program,
`CMD` as its default arguments).
## Build context and `.dockerignore`
`docker build -t mytag .` — that final `.` is the **build context**: the directory Docker can
see and copy files FROM during the build. A `.dockerignore` file (same idea as `.gitignore` from
ROOT 03) excludes files from the build context — keeping large or irrelevant files (like a local
`.git` folder or dependency caches) out of your image and speeding up builds.
## Layers, again
Each instruction in a Dockerfile creates one layer, cached individually. Docker reuses cached
layers for instructions that haven't changed since the last build — which is why the ORDER of
instructions matters for build speed: put things that change rarely (like `FROM` and installing
dependencies) before things that change often (like copying your actual application code), so a
small code change doesn't invalidate expensive earlier layers.
## Instructor Notes
**Lesson goal:** Give students enough Dockerfile literacy to write a real, small, working image
— not exhaustive coverage of every possible instruction.
**Approximate timing:** 18 minutes read; 10 minutes live.
**Common misconception to watch for:** copying application code before installing dependencies
— reinforce the "changes-rarely-first" layer-ordering principle with a concrete example.
**Demo idea:** build the same Dockerfile twice with no changes, showing the second build using
fully cached layers (`CACHED` in the output) and finishing almost instantly.
@@ -0,0 +1,62 @@
# Demonstration: Watching Layer Caching Actually Work
## OBSERVE
```
$ docker build -t demo:1 .
Step 1/3 : FROM alpine:3.19
Step 2/3 : WORKDIR /app
Step 3/3 : COPY greeting.txt .
Successfully tagged demo:1
```
## HYPOTHESIS
Predict: if you run the EXACT same build again with no changes, will it take the same amount of
time?
## INSPECT
```
$ docker build -t demo:1 .
Step 1/3 : FROM alpine:3.19
Step 2/3 : WORKDIR /app
---> Using cache
Step 3/3 : COPY greeting.txt .
---> Using cache
Successfully tagged demo:1
```
`Using cache` — nearly instant, because nothing actually changed since the last build.
## CHANGE
Now change `greeting.txt`'s content and rebuild:
```
$ echo "updated" > greeting.txt
$ docker build -t demo:1 .
Step 2/3 : WORKDIR /app
---> Using cache
Step 3/3 : COPY greeting.txt .
---> (this step re-runs — the file changed)
```
Only the step AFTER the change re-runs — `WORKDIR` (unaffected) still used its cache.
## VERIFY
This is exactly why instruction ORDER matters: everything before the first changed instruction
stays cached; everything from that point onward must rebuild. Put your least-frequently-changing
instructions first.
## Instructor Notes
**Demo goal:** Make layer caching a directly OBSERVED behavior (via real build output), not an
abstract efficiency claim.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** ordering a Dockerfile with frequently-changing application code
copied BEFORE installing rarely-changing dependencies — invalidates the expensive dependency-
install cache on every trivial code change.
@@ -0,0 +1,38 @@
# Knowledge Check: Dockerfile and Image Build
---
**1. What does `WORKDIR` actually set?**
*Answer:* The working directory for every subsequent instruction in the build, and the
container's default working directory when it runs.
---
**2. What is the "build context"?**
*Answer:* The directory (usually specified as `.` at the end of `docker build -t tag .`) that
Docker can see and copy files FROM during the build — `COPY` instructions read from it.
---
**3. Why does instruction order in a Dockerfile affect build speed?**
*Answer:* Docker caches each instruction's result as a layer and reuses cached layers for
unchanged instructions — everything before the first ACTUALLY-changed instruction stays cached,
so putting rarely-changing instructions (like dependency installs) before frequently-changing
ones (like application code) minimizes unnecessary rebuilding.
---
**4. What's `.dockerignore` for, and what other file does it resemble?**
*Answer:* It excludes files from the build context (keeping large/irrelevant files out of the
image and speeding up builds) — the same idea as ROOT 03's `.gitignore`.
## Instructor Notes
**Facilitation:** Question 3 directly reinforces the caching demo — a good check that the
concept, not just the observation, landed.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,35 @@
# Guided Exercise: Order These Instructions for Maximum Cache Efficiency
This is a paper exercise — commit to an order before checking it in the Lab.
## Your task
You need a Dockerfile with these five things, in SOME order. Application code changes very
often; dependencies change rarely. Write the most cache-efficient order:
- `FROM python:3.11-slim`
- `COPY app.py .` (your actual application code — changes often)
- `COPY requirements.txt .` (dependency list — changes rarely)
- `RUN pip install -r requirements.txt` (installs dependencies — slow)
- `WORKDIR /app`
## A worked shape to check yourself against
```
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app.py .
```
Copying `requirements.txt` and installing dependencies BEFORE copying the actual application
code means a code-only change never invalidates the (slow) dependency-install layer — only the
final `COPY app.py .` step needs to rerun.
## Instructor Notes
**Facilitation:** This is one of the most practically useful habits in the entire module — worth
connecting explicitly to real build-time savings on larger, slower dependency installs.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,24 @@
# Reflection: You Built a Real Image, Not Just Ran Someone Else's
## Reflection questions
1. Every image you used before this module (nginx, alpine) was built by someone else, from a
Dockerfile you never saw. Does writing your own change how you think about those images?
2. Which Dockerfile instruction do you expect to use the most in real application work?
## Real Ops takeaway
**A Dockerfile is executable documentation of exactly how an application's environment is
built — the same reproducibility promise ROOT 03's version control made about code, now applied
to environments.** You've closed the loop this course opened with Module 1's "it works on my
machine" problem: a Dockerfile means the environment itself is defined, versioned, and
rebuildable identically anywhere, by anyone, forever.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 is a good moment to reflect on the
"black box → understood mechanism" arc of this entire course.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,65 @@
# Docker Compose
## Real applications are rarely just one container
A web app usually needs a database, maybe a cache, maybe a background worker — several
containers that need to be created, networked together, and started in a sensible order. Doing
this manually with individual `docker run` commands works, but gets tedious and error-prone fast,
especially remembering the exact flags every time.
## One file, the whole application
**Docker Compose** describes an entire multi-container application in one YAML file:
```yaml
services:
web:
image: nginx:alpine
ports:
- "8080:80"
depends_on:
- api
api:
image: my-api:1
```
- **`services`** — each entry is one container, using the exact same concepts (image, ports,
etc.) you already know from plain `docker run`.
- **`depends_on`** — controls STARTUP order (api starts before web) — note this ensures order,
not that the dependency is fully READY yet, a subtlety real applications often need extra
handling for.
- **Networking is automatic** — Compose creates a shared network for all services in the file by
default, so `web` can reach `api` by name immediately, no manual `docker network create`
needed.
## The commands
```
docker compose up -d # start every service defined in the file, in the background
docker compose ps # see the status of every service
docker compose logs # see logs from every service, or docker compose logs <service>
docker compose down # stop and remove everything Compose created
```
## Compose doesn't replace anything you've learned — it automates it
Every Compose service maps directly onto a `docker run` command you could write by hand; Compose
just describes the whole set declaratively in one file instead of remembering and re-typing
several long commands correctly, every time, in the right order.
## Instructor Notes
**Lesson goal:** Frame Compose as automation of already-understood concepts, not new magic —
students should be able to mentally translate a `services:` block back into individual
`docker run` commands.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating `depends_on` as "waits until fully healthy" —
it only controls START order, not readiness; worth flagging as a real, common source of race
conditions in actual production Compose files.
**Discussion question:** "Why might describing your application declaratively in one file be
better than a shell script that runs several `docker run` commands in sequence?" (Good answer:
reviewable, versionable, and Compose itself understands the relationships between services,
rather than just blindly executing a sequence.)
@@ -0,0 +1,64 @@
# Demonstration: One File, Two Containers, One Network — Automatically
## OBSERVE
```yaml
services:
web:
image: nginx:alpine
ports:
- "8080:80"
cache:
image: redis:alpine
```
## HYPOTHESIS
Predict: without running `docker network create` yourself, will `web` be able to reach `cache`
by name after `docker compose up`?
## INSPECT
```
$ docker compose up -d
$ docker compose exec web sh -c "wget -qO- http://cache:6379/ 2>&1 | head -1"
```
(Redis doesn't speak HTTP, so this specific request looks odd, but the connection ITSELF
succeeding or failing tells you whether name resolution and network reachability work — a
`Connection refused` here would mean it REACHED `cache` but Redis rejected the protocol
mismatch, which is actually proof the network worked.)
## CHANGE
```
$ docker compose ps
NAME IMAGE STATUS
web-1 nginx:alpine Up
cache-1 redis:alpine Up
$ docker network ls | grep compose
xyz123 myproject_default bridge
```
Compose created a network automatically, named after the project directory, with no manual step.
## VERIFY
```
$ docker compose down
$ docker network ls | grep compose
(nothing)
```
`docker compose down` cleaned up EVERYTHING Compose created — containers AND the network — in
one command.
## Instructor Notes
**Demo goal:** Show the automatic networking as a real, observable fact (a real network exists,
named and scoped to the project) rather than an assumed convenience.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** forgetting `docker compose down` and accumulating orphaned
networks/containers across multiple exercises — a good habit to reinforce explicitly.
@@ -0,0 +1,40 @@
# Knowledge Check: Docker Compose
---
**1. What does `depends_on` actually guarantee — and NOT guarantee?**
*Answer:* It guarantees START order (the dependency's container is started before the dependent
service's). It does NOT guarantee the dependency is actually READY/healthy yet — a real,
common source of race conditions in production Compose files.
---
**2. Do you need to manually run `docker network create` for services in the same Compose file
to reach each other by name?**
*Answer:* No — Compose automatically creates a shared network for all services defined in the
file, and they can reach each other by service name immediately.
---
**3. What does `docker compose down` clean up, that plain `docker compose stop` would not?**
*Answer:* `down` removes the containers AND the network Compose created; `stop` only stops the
containers, leaving them (and the network) in place to be restarted later.
---
**4. Does Compose introduce any concept that isn't already just automating `docker run`/
`docker network` commands you already know?**
*Answer:* No — every Compose service maps directly onto concepts already covered
(image, ports, networking); Compose automates and declaratively describes them, it doesn't add
new underlying mechanisms.
## Instructor Notes
**Facilitation:** Question 1 is worth real emphasis — it's a genuine, common production
mistake worth preventing early.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,36 @@
# Guided Exercise: Translate a Compose File Back Into Plain `docker run`
This is a paper exercise — commit to your translation before checking it in the Lab.
## Your task
Given this Compose service definition, write the equivalent plain `docker run` command by hand:
```yaml
services:
web:
image: nginx:alpine
ports:
- "8080:80"
```
Then explain: what would you ALSO need to do manually (that Compose does automatically) if two
services needed to reach each other by name?
## A worked shape to check yourself against
```
docker run -d -p 8080:80 nginx:alpine
```
For two services to reach each other by name manually, you'd need to `docker network create` a
shared network yourself, then add `--network <that-network>` to EACH container's `docker run`
command — exactly the manual step Compose eliminates.
## Instructor Notes
**Facilitation:** This exercise is the clearest possible proof that Compose isn't new magic —
being able to translate it back to `docker run` confirms genuine understanding, not just
familiarity with YAML syntax.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,26 @@
# Reflection: A Real Multi-Service Application, Described and Running
## Reflection questions
1. Before this module, how would you have run two related containers and connected them
manually? How does that compare to this module's Lab?
2. What does a Compose file's `services:` block remind you of, from ROOT_'s later Kubernetes
content — even without knowing Kubernetes syntax yet?
## Real Ops takeaway
**You just described and ran a real multi-container application declaratively — the exact same
"describe what you want, let the tool make it real" pattern behind every later Kubernetes manifest
in this program.** This isn't a coincidence: Compose and Kubernetes both solve "orchestrate
multiple containers as one application," at different scales. The habit of thinking in terms of
declared services rather than manual step-by-step commands is what ROOT 05 builds on directly.
This module is complete. Continue to the final module of this course.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 previews Kubernetes manifests
directly — a good moment to name "declarative description of desired state" as a term students
will hear constantly starting in ROOT 05.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,64 @@
# Containers → Kubernetes
## What you've actually built, across this entire course
You can now package an application (Dockerfile), run it in isolation (containers), connect
multiple containers together (networking, Compose), and persist their data reliably (volumes).
That's a complete, real skill set for running an application on ONE machine. Kubernetes exists
to answer the question this course deliberately never had to ask: **what happens when "one
machine" isn't enough?**
## What Docker already solves
- **Application packaging** — a Dockerfile makes an environment reproducible anywhere.
- **Process isolation** — a container is isolated from other processes on the same machine.
- **Basic networking** — containers can reach each other and be reached from outside, on one
host.
- **Basic multi-container orchestration** — Compose runs a small set of related containers
together, on one host.
## What Kubernetes adds on top
- **Scheduling** — deciding WHICH machine, out of potentially hundreds, a container should run
on — a question that doesn't exist when you only have one machine.
- **Desired state and reconciliation** — you declare what SHOULD be running; Kubernetes
continuously works to make reality match that declaration, even after a machine fails.
- **Controllers** — automated processes that watch and correct drift from desired state (the
generalized version of "restart it if it crashes," running constantly, across the whole
cluster).
- **Service discovery at scale** — reaching "the right instance of a service" when there might
be dozens of them, spread across many machines, changing constantly.
- **Health management** — automatically detecting an unhealthy container and replacing it,
without a human first noticing something's wrong.
- **Scaling** — running more or fewer copies of something based on real demand, automatically.
- **Rollout strategies** — deploying a new version across many running instances safely, with a
defined strategy for how to do it without an outage.
- **Distributed orchestration** — coordinating ALL of the above across a fleet of machines as if
it were one system.
## The honest bridge
None of this makes Docker/containers obsolete — Kubernetes runs containers, using the exact
container runtime concepts this course just taught. Kubernetes is the layer ABOVE containers
that exists specifically because real production systems eventually need more than one machine,
more reliability than manual restarts, and more automation than a human watching dashboards. You
are not about to learn a replacement for what you just built — you're about to learn what
happens when you need to run it at a scale one machine can't handle.
## Instructor Notes
**Lesson goal:** Deliver the explicit "why Kubernetes" answer this entire foundational half of
ROOT_ has been building toward — students should be able to state, in their own words, what
specifically Kubernetes adds that Docker alone doesn't.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating Kubernetes as "a replacement for Docker" rather
than "an orchestration layer that runs containers across many machines" — the container
concepts from this entire course remain fully applicable inside Kubernetes.
**Discussion question:** "Name one thing from this list (scheduling, desired state, controllers,
service discovery, health management, scaling, rollout, distributed orchestration) that would be
genuinely hard to do by hand with just `docker run` commands across many machines." (Almost any
honest answer works — the point is feeling the real gap Kubernetes fills, not memorizing the
list.)
@@ -0,0 +1,40 @@
# Knowledge Check: Containers → Kubernetes
---
**1. Does Kubernetes replace Docker/containers?**
*Answer:* No — Kubernetes orchestrates containers across many machines; it runs on top of the
same container concepts this course taught, not instead of them.
---
**2. Name one thing Kubernetes adds that a single-machine `docker run`/Compose setup
fundamentally can't do.**
*Answer:* Any reasonable answer from: scheduling across multiple machines, automatic
reconciliation to a desired state after a failure, service discovery across many changing
instances, automated health-based replacement, demand-based scaling, or coordinated rollout
across a fleet.
---
**3. What does "desired state" mean, in the sense this lesson introduced it?**
*Answer:* You declare what SHOULD be running; the orchestrator continuously works to make actual
reality match that declaration, including automatically after a failure — rather than a person
having to manually notice and fix drift.
---
**4. Why doesn't this course teach Kubernetes directly, ending here instead?**
*Answer:* This course's job was building the container/Linux/Git foundation Kubernetes assumes
— ROOT 05 builds on exactly this foundation to teach Kubernetes itself.
## Instructor Notes
**Facilitation:** Question 2 is worth collecting several different answers from the group — the
variety itself is evidence the "why Kubernetes" question actually landed.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,33 @@
# Reflection: You're Ready, By Design, Not By Accident
## Reflection questions
1. Look back across ROOT 01 (Linux/processes), ROOT 02 (networking/troubleshooting), ROOT 03
(Git), and ROOT 04 (containers). Which single skill do you expect to use MOST once you start
ROOT 05?
2. Can you explain, in your own words and without looking anything up, why Kubernetes exists?
If you can't yet, which part of this module's lesson would you reread?
## Real Ops takeaway
**Every module across all four foundational courses was chosen deliberately to make THIS moment
make sense.** Processes (ROOT 01) become container processes become Pods. Filesystem/storage
(ROOT 01-02) becomes container volumes becomes Kubernetes storage. Networking/DNS/troubleshooting
(ROOT 02) becomes container networking becomes Services/Ingress/NetworkPolicy. Git (ROOT 03)
becomes the foundation for GitOps later. None of ROOT 05's content will be starting from zero —
you are walking in already equipped, on purpose.
This module — and the entire ROOT_ Foundations sequence (ROOT 01-04) — is complete. Continue to
ROOT 05: Kubernetes Foundations.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 is the single best readiness check for
this entire foundational sequence — a confident, correct answer here is real evidence students
are ready for ROOT 05, not just that they clicked through four courses.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced writing.
**Milestone note:** this is the last module of the entire ROOT_ Foundations sequence (ROOT 01-04)
— consider a genuine celebratory moment here for a live cohort; this represents a real, complete
skill foundation, not just a checkpoint.