diff --git a/content/root/root-04-containers-and-docker/m01-why-containers-exist/01-lesson-why-containers-exist.md b/content/root/root-04-containers-and-docker/m01-why-containers-exist/01-lesson-why-containers-exist.md new file mode 100644 index 0000000..7851fdd --- /dev/null +++ b/content/root/root-04-containers-and-docker/m01-why-containers-exist/01-lesson-why-containers-exist.md @@ -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.) diff --git a/content/root/root-04-containers-and-docker/m01-why-containers-exist/02-demonstration-shared-kernel-vs-full-vm.md b/content/root/root-04-containers-and-docker/m01-why-containers-exist/02-demonstration-shared-kernel-vs-full-vm.md new file mode 100644 index 0000000..ba239a4 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m01-why-containers-exist/02-demonstration-shared-kernel-vs-full-vm.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m01-why-containers-exist/03-knowledge-check.md b/content/root/root-04-containers-and-docker/m01-why-containers-exist/03-knowledge-check.md new file mode 100644 index 0000000..4644f38 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m01-why-containers-exist/03-knowledge-check.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m01-why-containers-exist/04-reflection.md b/content/root/root-04-containers-and-docker/m01-why-containers-exist/04-reflection.md new file mode 100644 index 0000000..6446772 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m01-why-containers-exist/04-reflection.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m02-images-and-containers/01-lesson-images-and-containers.md b/content/root/root-04-containers-and-docker/m02-images-and-containers/01-lesson-images-and-containers.md new file mode 100644 index 0000000..f5088df --- /dev/null +++ b/content/root/root-04-containers-and-docker/m02-images-and-containers/01-lesson-images-and-containers.md @@ -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 `** — download an image from a registry, without running it. +- **`docker run `** — create and start a NEW container from an image (pulls automatically + if not already present locally). +- **`docker stop `** — gracefully stop a running container (its process, not the + image). +- **`docker rm `** — 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.) diff --git a/content/root/root-04-containers-and-docker/m02-images-and-containers/02-demonstration-one-image-many-containers.md b/content/root/root-04-containers-and-docker/m02-images-and-containers/02-demonstration-one-image-many-containers.md new file mode 100644 index 0000000..47dae27 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m02-images-and-containers/02-demonstration-one-image-many-containers.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m02-images-and-containers/03-knowledge-check.md b/content/root/root-04-containers-and-docker/m02-images-and-containers/03-knowledge-check.md new file mode 100644 index 0000000..2130099 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m02-images-and-containers/03-knowledge-check.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m02-images-and-containers/04-guided-exercise-tracing-image-and-container-counts.md b/content/root/root-04-containers-and-docker/m02-images-and-containers/04-guided-exercise-tracing-image-and-container-counts.md new file mode 100644 index 0000000..52130db --- /dev/null +++ b/content/root/root-04-containers-and-docker/m02-images-and-containers/04-guided-exercise-tracing-image-and-container-counts.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m02-images-and-containers/05-reflection.md b/content/root/root-04-containers-and-docker/m02-images-and-containers/05-reflection.md new file mode 100644 index 0000000..18a163b --- /dev/null +++ b/content/root/root-04-containers-and-docker/m02-images-and-containers/05-reflection.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m03-docker-cli/01-lesson-docker-cli.md b/content/root/root-04-containers-and-docker/m03-docker-cli/01-lesson-docker-cli.md new file mode 100644 index 0000000..8d843c7 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m03-docker-cli/01-lesson-docker-cli.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m03-docker-cli/02-demonstration-run-vs-exec.md b/content/root/root-04-containers-and-docker/m03-docker-cli/02-demonstration-run-vs-exec.md new file mode 100644 index 0000000..181225f --- /dev/null +++ b/content/root/root-04-containers-and-docker/m03-docker-cli/02-demonstration-run-vs-exec.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m03-docker-cli/03-knowledge-check.md b/content/root/root-04-containers-and-docker/m03-docker-cli/03-knowledge-check.md new file mode 100644 index 0000000..393b636 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m03-docker-cli/03-knowledge-check.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m03-docker-cli/04-guided-exercise-choosing-the-right-docker-command.md b/content/root/root-04-containers-and-docker/m03-docker-cli/04-guided-exercise-choosing-the-right-docker-command.md new file mode 100644 index 0000000..d69dc33 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m03-docker-cli/04-guided-exercise-choosing-the-right-docker-command.md @@ -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 ` +3. `docker logs ` +4. `docker inspect --format '{{...}}'` +5. `docker rm -f ` + +## 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. diff --git a/content/root/root-04-containers-and-docker/m03-docker-cli/05-reflection.md b/content/root/root-04-containers-and-docker/m03-docker-cli/05-reflection.md new file mode 100644 index 0000000..1418455 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m03-docker-cli/05-reflection.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m04-container-networking/01-lesson-container-networking.md b/content/root/root-04-containers-and-docker/m04-container-networking/01-lesson-container-networking.md new file mode 100644 index 0000000..8f74320 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m04-container-networking/01-lesson-container-networking.md @@ -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 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.) diff --git a/content/root/root-04-containers-and-docker/m04-container-networking/02-demonstration-container-to-container-by-name.md b/content/root/root-04-containers-and-docker/m04-container-networking/02-demonstration-container-to-container-by-name.md new file mode 100644 index 0000000..d85cc75 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m04-container-networking/02-demonstration-container-to-container-by-name.md @@ -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" + +``` + +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. diff --git a/content/root/root-04-containers-and-docker/m04-container-networking/03-knowledge-check.md b/content/root/root-04-containers-and-docker/m04-container-networking/03-knowledge-check.md new file mode 100644 index 0000000..c19b9f8 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m04-container-networking/03-knowledge-check.md @@ -0,0 +1,40 @@ +# Knowledge Check: Container Networking + +--- + +**1. A container works fine when tested with `docker exec 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. diff --git a/content/root/root-04-containers-and-docker/m04-container-networking/04-guided-exercise-diagnosing-before-fixing.md b/content/root/root-04-containers-and-docker/m04-container-networking/04-guided-exercise-diagnosing-before-fixing.md new file mode 100644 index 0000000..7bcdfed --- /dev/null +++ b/content/root/root-04-containers-and-docker/m04-container-networking/04-guided-exercise-diagnosing-before-fixing.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m04-container-networking/05-reflection.md b/content/root/root-04-containers-and-docker/m04-container-networking/05-reflection.md new file mode 100644 index 0000000..83b6b8f --- /dev/null +++ b/content/root/root-04-containers-and-docker/m04-container-networking/05-reflection.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m05-container-storage/01-lesson-container-storage.md b/content/root/root-04-containers-and-docker/m05-container-storage/01-lesson-container-storage.md new file mode 100644 index 0000000..25c497e --- /dev/null +++ b/content/root/root-04-containers-and-docker/m05-container-storage/01-lesson-container-storage.md @@ -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.) diff --git a/content/root/root-04-containers-and-docker/m05-container-storage/02-demonstration-data-loss-then-persistence.md b/content/root/root-04-containers-and-docker/m05-container-storage/02-demonstration-data-loss-then-persistence.md new file mode 100644 index 0000000..fd1753c --- /dev/null +++ b/content/root/root-04-containers-and-docker/m05-container-storage/02-demonstration-data-loss-then-persistence.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m05-container-storage/03-knowledge-check.md b/content/root/root-04-containers-and-docker/m05-container-storage/03-knowledge-check.md new file mode 100644 index 0000000..ca3c3bd --- /dev/null +++ b/content/root/root-04-containers-and-docker/m05-container-storage/03-knowledge-check.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m05-container-storage/04-guided-exercise-predicting-what-survives.md b/content/root/root-04-containers-and-docker/m05-container-storage/04-guided-exercise-predicting-what-survives.md new file mode 100644 index 0000000..3584c55 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m05-container-storage/04-guided-exercise-predicting-what-survives.md @@ -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 ` 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. diff --git a/content/root/root-04-containers-and-docker/m05-container-storage/05-reflection.md b/content/root/root-04-containers-and-docker/m05-container-storage/05-reflection.md new file mode 100644 index 0000000..fc57669 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m05-container-storage/05-reflection.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/01-lesson-dockerfile-and-image-build.md b/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/01-lesson-dockerfile-and-image-build.md new file mode 100644 index 0000000..bf4b1e2 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/01-lesson-dockerfile-and-image-build.md @@ -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 `** — every Dockerfile starts by choosing a base image to build on top of. + Almost nothing is built completely from scratch. +- **`WORKDIR `** — sets the working directory for every instruction that follows it (and + for the container's default working directory when it runs). +- **`COPY `** — copies files from your BUILD CONTEXT (the directory you + run `docker build` from) into the image. +- **`RUN `** — executes a command DURING the build, creating a new layer with its + result (installing a package, for example). +- **`CMD `** (conceptually) — sets the DEFAULT command a container runs when started + from this image, if nothing else is specified. +- **`ENTRYPOINT `** (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. diff --git a/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/02-demonstration-layer-caching-in-action.md b/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/02-demonstration-layer-caching-in-action.md new file mode 100644 index 0000000..17c1b9b --- /dev/null +++ b/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/02-demonstration-layer-caching-in-action.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/03-knowledge-check.md b/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/03-knowledge-check.md new file mode 100644 index 0000000..815ec33 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/03-knowledge-check.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/04-guided-exercise-ordering-instructions-for-cache-efficiency.md b/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/04-guided-exercise-ordering-instructions-for-cache-efficiency.md new file mode 100644 index 0000000..f22b74f --- /dev/null +++ b/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/04-guided-exercise-ordering-instructions-for-cache-efficiency.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/05-reflection.md b/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/05-reflection.md new file mode 100644 index 0000000..06048b4 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/05-reflection.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m07-docker-compose/01-lesson-docker-compose.md b/content/root/root-04-containers-and-docker/m07-docker-compose/01-lesson-docker-compose.md new file mode 100644 index 0000000..f9de6d3 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m07-docker-compose/01-lesson-docker-compose.md @@ -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 +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.) diff --git a/content/root/root-04-containers-and-docker/m07-docker-compose/02-demonstration-one-file-two-services.md b/content/root/root-04-containers-and-docker/m07-docker-compose/02-demonstration-one-file-two-services.md new file mode 100644 index 0000000..6472e52 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m07-docker-compose/02-demonstration-one-file-two-services.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m07-docker-compose/03-knowledge-check.md b/content/root/root-04-containers-and-docker/m07-docker-compose/03-knowledge-check.md new file mode 100644 index 0000000..bf1e00f --- /dev/null +++ b/content/root/root-04-containers-and-docker/m07-docker-compose/03-knowledge-check.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m07-docker-compose/04-guided-exercise-translating-compose-to-run-commands.md b/content/root/root-04-containers-and-docker/m07-docker-compose/04-guided-exercise-translating-compose-to-run-commands.md new file mode 100644 index 0000000..eb2646f --- /dev/null +++ b/content/root/root-04-containers-and-docker/m07-docker-compose/04-guided-exercise-translating-compose-to-run-commands.md @@ -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 ` 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. diff --git a/content/root/root-04-containers-and-docker/m07-docker-compose/05-reflection.md b/content/root/root-04-containers-and-docker/m07-docker-compose/05-reflection.md new file mode 100644 index 0000000..1b4c0f1 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m07-docker-compose/05-reflection.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m08-containers-to-kubernetes/01-lesson-containers-to-kubernetes.md b/content/root/root-04-containers-and-docker/m08-containers-to-kubernetes/01-lesson-containers-to-kubernetes.md new file mode 100644 index 0000000..2092762 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m08-containers-to-kubernetes/01-lesson-containers-to-kubernetes.md @@ -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.) diff --git a/content/root/root-04-containers-and-docker/m08-containers-to-kubernetes/02-knowledge-check.md b/content/root/root-04-containers-and-docker/m08-containers-to-kubernetes/02-knowledge-check.md new file mode 100644 index 0000000..fcb5eab --- /dev/null +++ b/content/root/root-04-containers-and-docker/m08-containers-to-kubernetes/02-knowledge-check.md @@ -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. diff --git a/content/root/root-04-containers-and-docker/m08-containers-to-kubernetes/03-reflection.md b/content/root/root-04-containers-and-docker/m08-containers-to-kubernetes/03-reflection.md new file mode 100644 index 0000000..bbaf444 --- /dev/null +++ b/content/root/root-04-containers-and-docker/m08-containers-to-kubernetes/03-reflection.md @@ -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. diff --git a/platform/api/migrations/028_phase6e_root04_containers_and_docker.sql b/platform/api/migrations/028_phase6e_root04_containers_and_docker.sql new file mode 100644 index 0000000..aec350d --- /dev/null +++ b/platform/api/migrations/028_phase6e_root04_containers_and_docker.sql @@ -0,0 +1,156 @@ +-- Phase 6E — ROOT 04: Containers & Docker. Adds 8 modules/activities under the existing +-- (PLANNED, zero-content) root-04-containers-and-docker course row. Every lab activity uses the +-- nested Docker Engine built and tested in Phase 6D (docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §3) +-- via the existing generic scenario/validation engine — no new mechanism, no runtime redesign. +-- Modules 1, 2, and 8 are conceptual (no lab), matching the flexible readiness standard already +-- used throughout ROOT 01-03. + +UPDATE courses SET + status = 'ready', + objectives = '{"Explain what problem containers solve and how they differ from VMs","Distinguish images from containers precisely","Use the core Docker CLI fluently","Diagnose and fix a real container networking failure","Prove data persistence with a real destroy/recreate cycle","Write a Dockerfile and build a real image","Run a real multi-service application with Compose","Explain what Kubernetes adds on top of what Docker already solves"}', + updated_at = now() +WHERE course_id = 'root-04-containers-and-docker'; + +INSERT INTO course_modules (module_id, course_id, title, description, sequence, objectives, prerequisites, estimated_duration_minutes, status) VALUES +('root-04-m01-why-containers-exist', 'root-04-containers-and-docker', 'Why Containers Exist', + '"It works on my machine," dependency inconsistency, and VM vs. container. Target learner: has completed ROOT 03. No lab — motivational/conceptual opener.', + 1, '{"Explain the dependency-inconsistency problem containers solve","Explain the real architectural difference between a VM and a container"}', '{}', 25, 'ready'), +('root-04-m02-images-and-containers', 'root-04-containers-and-docker', 'Images & Containers', + 'The single most important distinction in this course: image (artifact) vs. container (running instance). No lab — conceptual model before hands-on commands.', + 2, '{"State the image/container distinction precisely","Explain layers, registries, repositories, and tags"}', '{root-04-m01-why-containers-exist}', 35, 'ready'), +('root-04-m03-docker-cli', 'root-04-containers-and-docker', 'Docker CLI', + 'pull/run/ps/logs/exec/inspect/stop/rm — each mapped directly to a ROOT 01/02 Linux concept already known.', + 3, '{"Run, inspect, and exec into a real container","Distinguish docker run from docker exec","Stop and remove containers correctly"}', '{root-04-m02-images-and-containers}', 40, 'ready'), +('root-04-m04-container-networking', 'root-04-containers-and-docker', 'Container Networking', + 'ROOT 02''s networking model, one layer up: container IPs, name-based DNS, and port publishing. Diagnose a real "works internally, unreachable externally" failure.', + 4, '{"Explain container-to-container name resolution","Explain what port publishing actually does","Diagnose and fix a real port-publishing failure"}', '{root-04-m03-docker-cli}', 45, 'ready'), +('root-04-m05-container-storage', 'root-04-containers-and-docker', 'Container Storage', + 'The disposable writable layer, bind mounts, and named volumes — proven with a real destroy/recreate cycle, not just described.', + 5, '{"Explain why container data is disposable by default","Distinguish bind mounts from named volumes","Prove data persistence with a real destroy/recreate test"}', '{root-04-m04-container-networking}', 40, 'ready'), +('root-04-m06-dockerfile-and-image-build', 'root-04-containers-and-docker', 'Dockerfile & Image Build', + 'Write a real Dockerfile, understand layer caching, and build a real image.', + 6, '{"Write a working Dockerfile using FROM/WORKDIR/COPY/RUN/CMD","Explain layer caching and instruction ordering","Build and run a real custom image"}', '{root-04-m05-container-storage}', 45, 'ready'), +('root-04-m07-docker-compose', 'root-04-containers-and-docker', 'Docker Compose', + 'Describe and run a real multi-service application declaratively.', + 7, '{"Write a Compose file with multiple services","Explain what depends_on does and does not guarantee","Run and verify a real two-service application"}', '{root-04-m06-dockerfile-and-image-build}', 40, 'ready'), +('root-04-m08-containers-to-kubernetes', 'root-04-containers-and-docker', 'Containers → Kubernetes', + 'The explicit bridge module: what Kubernetes adds on top of what Docker already solves. No lab — the honest, deliberate close of the ROOT_ Foundations sequence.', + 8, '{"Explain what Kubernetes adds beyond single-machine container orchestration","Articulate why Kubernetes exists, in one''s own words"}', '{root-04-m07-docker-compose}', 30, 'ready') +ON CONFLICT (module_id) DO NOTHING; + +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +-- M01 (no lab, no guided) +('root-04-m01-a01-lesson', 'root-04-m01-why-containers-exist', 1, 'lesson', 'Why Containers Exist', + 'content/root/root-04-containers-and-docker/m01-why-containers-exist/01-lesson-why-containers-exist.md', NULL, NULL, 'manual_marker', 12, 'ready'), +('root-04-m01-a02-demonstration', 'root-04-m01-why-containers-exist', 2, 'demonstration', 'Demonstration: What "Shares the Host Kernel" Actually Looks Like', + 'content/root/root-04-containers-and-docker/m01-why-containers-exist/02-demonstration-shared-kernel-vs-full-vm.md', NULL, NULL, 'manual_marker', 8, 'ready'), +('root-04-m01-a03-knowledge-check', 'root-04-m01-why-containers-exist', 3, 'knowledge_check', 'Knowledge Check: Why Containers Exist', + 'content/root/root-04-containers-and-docker/m01-why-containers-exist/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-04-m01-a04-reflection', 'root-04-m01-why-containers-exist', 4, 'reflection', 'Reflection: You Now Know What Problem You''re About to Solve, Hands-On', + 'content/root/root-04-containers-and-docker/m01-why-containers-exist/04-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'), +-- M02 (no lab) +('root-04-m02-a01-lesson', 'root-04-m02-images-and-containers', 1, 'lesson', 'Images and Containers', + 'content/root/root-04-containers-and-docker/m02-images-and-containers/01-lesson-images-and-containers.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-04-m02-a02-demonstration', 'root-04-m02-images-and-containers', 2, 'demonstration', 'Demonstration: One Image, Many Independent Containers', + 'content/root/root-04-containers-and-docker/m02-images-and-containers/02-demonstration-one-image-many-containers.md', NULL, NULL, 'manual_marker', 8, 'ready'), +('root-04-m02-a03-knowledge-check', 'root-04-m02-images-and-containers', 3, 'knowledge_check', 'Knowledge Check: Images and Containers', + 'content/root/root-04-containers-and-docker/m02-images-and-containers/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-04-m02-a04-guided-exercise', 'root-04-m02-images-and-containers', 4, 'guided_exercise', 'Guided Exercise: Trace the Counts Through a Sequence of Commands', + 'content/root/root-04-containers-and-docker/m02-images-and-containers/04-guided-exercise-tracing-image-and-container-counts.md', NULL, NULL, 'manual_marker', 8, 'ready'), +('root-04-m02-a05-reflection', 'root-04-m02-images-and-containers', 5, 'reflection', 'Reflection: A Small Distinction With Large Consequences', + 'content/root/root-04-containers-and-docker/m02-images-and-containers/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'), +-- M03 +('root-04-m03-a01-lesson', 'root-04-m03-docker-cli', 1, 'lesson', 'Docker CLI', + 'content/root/root-04-containers-and-docker/m03-docker-cli/01-lesson-docker-cli.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-04-m03-a02-demonstration', 'root-04-m03-docker-cli', 2, 'demonstration', 'Demonstration: run Creates, exec Reaches In', + 'content/root/root-04-containers-and-docker/m03-docker-cli/02-demonstration-run-vs-exec.md', NULL, NULL, 'manual_marker', 8, 'ready'), +('root-04-m03-a03-knowledge-check', 'root-04-m03-docker-cli', 3, 'knowledge_check', 'Knowledge Check: Docker CLI', + 'content/root/root-04-containers-and-docker/m03-docker-cli/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-04-m03-a04-guided-exercise', 'root-04-m03-docker-cli', 4, 'guided_exercise', 'Guided Exercise: Which Docker Command Actually Does This?', + 'content/root/root-04-containers-and-docker/m03-docker-cli/04-guided-exercise-choosing-the-right-docker-command.md', NULL, NULL, 'manual_marker', 8, 'ready'), +('root-04-m03-a05-lab-cli-basics', 'root-04-m03-docker-cli', 5, 'lab', 'Lab: Run, Exec Into, and Inspect a Real Container', + NULL, 'docker-cli-basics', NULL, 'auto_scenario', 12, 'ready'), +('root-04-m03-a06-reflection', 'root-04-m03-docker-cli', 6, 'reflection', 'Reflection: The Whole Toolkit Was Already Familiar', + 'content/root/root-04-containers-and-docker/m03-docker-cli/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'), +-- M04 +('root-04-m04-a01-lesson', 'root-04-m04-container-networking', 1, 'lesson', 'Container Networking', + 'content/root/root-04-containers-and-docker/m04-container-networking/01-lesson-container-networking.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-04-m04-a02-demonstration', 'root-04-m04-container-networking', 2, 'demonstration', 'Demonstration: Reaching a Container By Name, Not IP', + 'content/root/root-04-containers-and-docker/m04-container-networking/02-demonstration-container-to-container-by-name.md', NULL, NULL, 'manual_marker', 8, 'ready'), +('root-04-m04-a03-knowledge-check', 'root-04-m04-container-networking', 3, 'knowledge_check', 'Knowledge Check: Container Networking', + 'content/root/root-04-containers-and-docker/m04-container-networking/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-04-m04-a04-guided-exercise', 'root-04-m04-container-networking', 4, 'guided_exercise', 'Guided Exercise: Diagnose Before You Recreate Anything', + 'content/root/root-04-containers-and-docker/m04-container-networking/04-guided-exercise-diagnosing-before-fixing.md', NULL, NULL, 'manual_marker', 8, 'ready'), +('root-04-m04-a05-lab-container-networking', 'root-04-m04-container-networking', 5, 'lab', 'Lab: It Works Inside the Container, But Not From the Host — Why?', + NULL, 'docker-container-networking', NULL, 'auto_scenario', 15, 'ready'), +('root-04-m04-a06-reflection', 'root-04-m04-container-networking', 6, 'reflection', 'Reflection: Same Diagnosis, New Layer', + 'content/root/root-04-containers-and-docker/m04-container-networking/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'), +-- M05 +('root-04-m05-a01-lesson', 'root-04-m05-container-storage', 1, 'lesson', 'Container Storage', + 'content/root/root-04-containers-and-docker/m05-container-storage/01-lesson-container-storage.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-04-m05-a02-demonstration', 'root-04-m05-container-storage', 2, 'demonstration', 'Demonstration: Proving Data Loss, Then Proving Persistence', + 'content/root/root-04-containers-and-docker/m05-container-storage/02-demonstration-data-loss-then-persistence.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-04-m05-a03-knowledge-check', 'root-04-m05-container-storage', 3, 'knowledge_check', 'Knowledge Check: Container Storage', + 'content/root/root-04-containers-and-docker/m05-container-storage/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-04-m05-a04-guided-exercise', 'root-04-m05-container-storage', 4, 'guided_exercise', 'Guided Exercise: Predict What Survives Before You Test It', + 'content/root/root-04-containers-and-docker/m05-container-storage/04-guided-exercise-predicting-what-survives.md', NULL, NULL, 'manual_marker', 8, 'ready'), +('root-04-m05-a05-lab-container-storage', 'root-04-m05-container-storage', 5, 'lab', 'Lab: Prove Data Survives a Container''s Death — With a Volume', + NULL, 'docker-container-storage', NULL, 'auto_scenario', 15, 'ready'), +('root-04-m05-a06-reflection', 'root-04-m05-container-storage', 6, 'reflection', 'Reflection: You Proved Persistence, You Didn''t Just Trust It', + 'content/root/root-04-containers-and-docker/m05-container-storage/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'), +-- M06 +('root-04-m06-a01-lesson', 'root-04-m06-dockerfile-and-image-build', 1, 'lesson', 'Dockerfile and Image Build', + 'content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/01-lesson-dockerfile-and-image-build.md', NULL, NULL, 'manual_marker', 18, 'ready'), +('root-04-m06-a02-demonstration', 'root-04-m06-dockerfile-and-image-build', 2, 'demonstration', 'Demonstration: Watching Layer Caching Actually Work', + 'content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/02-demonstration-layer-caching-in-action.md', NULL, NULL, 'manual_marker', 8, 'ready'), +('root-04-m06-a03-knowledge-check', 'root-04-m06-dockerfile-and-image-build', 3, 'knowledge_check', 'Knowledge Check: Dockerfile and Image Build', + 'content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-04-m06-a04-guided-exercise', 'root-04-m06-dockerfile-and-image-build', 4, 'guided_exercise', 'Guided Exercise: Order These Instructions for Maximum Cache Efficiency', + 'content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/04-guided-exercise-ordering-instructions-for-cache-efficiency.md', NULL, NULL, 'manual_marker', 8, 'ready'), +('root-04-m06-a05-lab-build-image', 'root-04-m06-dockerfile-and-image-build', 5, 'lab', 'Lab: Write a Real Dockerfile and Build a Real Image', + NULL, 'docker-build-image', NULL, 'auto_scenario', 15, 'ready'), +('root-04-m06-a06-reflection', 'root-04-m06-dockerfile-and-image-build', 6, 'reflection', 'Reflection: You Built a Real Image, Not Just Ran Someone Else''s', + 'content/root/root-04-containers-and-docker/m06-dockerfile-and-image-build/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'), +-- M07 +('root-04-m07-a01-lesson', 'root-04-m07-docker-compose', 1, 'lesson', 'Docker Compose', + 'content/root/root-04-containers-and-docker/m07-docker-compose/01-lesson-docker-compose.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-04-m07-a02-demonstration', 'root-04-m07-docker-compose', 2, 'demonstration', 'Demonstration: One File, Two Containers, One Network — Automatically', + 'content/root/root-04-containers-and-docker/m07-docker-compose/02-demonstration-one-file-two-services.md', NULL, NULL, 'manual_marker', 8, 'ready'), +('root-04-m07-a03-knowledge-check', 'root-04-m07-docker-compose', 3, 'knowledge_check', 'Knowledge Check: Docker Compose', + 'content/root/root-04-containers-and-docker/m07-docker-compose/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-04-m07-a04-guided-exercise', 'root-04-m07-docker-compose', 4, 'guided_exercise', 'Guided Exercise: Translate a Compose File Back Into Plain docker run', + 'content/root/root-04-containers-and-docker/m07-docker-compose/04-guided-exercise-translating-compose-to-run-commands.md', NULL, NULL, 'manual_marker', 8, 'ready'), +('root-04-m07-a05-lab-compose-multiservice', 'root-04-m07-docker-compose', 5, 'lab', 'Lab: Run a Real Two-Service Application With Compose', + NULL, 'docker-compose-multiservice', NULL, 'auto_scenario', 15, 'ready'), +('root-04-m07-a06-reflection', 'root-04-m07-docker-compose', 6, 'reflection', 'Reflection: A Real Multi-Service Application, Described and Running', + 'content/root/root-04-containers-and-docker/m07-docker-compose/05-reflection.md', NULL, NULL, 'manual_marker', 8, 'ready'), +-- M08 (no lab, no demo, no guided — bridge module) +('root-04-m08-a01-lesson', 'root-04-m08-containers-to-kubernetes', 1, 'lesson', 'Containers → Kubernetes', + 'content/root/root-04-containers-and-docker/m08-containers-to-kubernetes/01-lesson-containers-to-kubernetes.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-04-m08-a02-knowledge-check', 'root-04-m08-containers-to-kubernetes', 2, 'knowledge_check', 'Knowledge Check: Containers → Kubernetes', + 'content/root/root-04-containers-and-docker/m08-containers-to-kubernetes/02-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-04-m08-a03-reflection', 'root-04-m08-containers-to-kubernetes', 3, 'reflection', 'Reflection: You''re Ready, By Design, Not By Accident', + 'content/root/root-04-containers-and-docker/m08-containers-to-kubernetes/03-reflection.md', NULL, NULL, 'manual_marker', 10, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- ROOT 04 course assessment — mixes real Docker-state-validation tasks with knowledge-recall +-- MCQs reasoning about image/container distinction and port-publishing failures, via the +-- existing Phase 3B engine. +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +INSERT INTO assessment_definitions (assessment_definition_id, type, title, description, duration_minutes, hint_policy, pool_spec, completion_rule) +VALUES ( + 'rootnode-containers-assessment', 'exam', 'ROOT 04 Containers & Docker Assessment', + 'A timed check across the Docker CLI, container networking/storage diagnosis, image builds, and Compose — combining real container-state-validation tasks with conceptual reasoning questions.', + 40, 'disabled', + '{"required": ["docker-cli-basics", "docker-build-image"], + "random": [ + {"count":2,"from":["docker-container-networking","docker-container-storage","docker-compose-multiservice"]}, + {"count":2,"from":["docker-image-vs-container-mcq","docker-port-publish-mcq"]} + ]}', + '{"type":"all_scenarios_passed"}' +) ON CONFLICT (assessment_definition_id) DO NOTHING; + +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-04-m08-a04-assessment', 'root-04-m08-containers-to-kubernetes', 4, 'assessment', 'ROOT 04 Containers & Docker Assessment', NULL, NULL, 'rootnode-containers-assessment', 'auto_assessment', 40, 'ready') +ON CONFLICT (activity_id) DO NOTHING; diff --git a/scenarios/data/docker-build-image.json b/scenarios/data/docker-build-image.json new file mode 100644 index 0000000..d68ffb7 --- /dev/null +++ b/scenarios/data/docker-build-image.json @@ -0,0 +1,37 @@ +{ + "id": "docker-build-image", + "title": "Write a Real Dockerfile and Build a Real Image", + "category": "Containers", + "isolation_type": "shared_namespace", + "difficulty": "Medium", + "type": "task", + "weight": 6, + "description": "## Write a Real Dockerfile and Build a Real Image\n\nA file `/srv/build-lab/greeting.txt` already exists, containing a message.\n\n**Your task:**\n\n1. Write a `Dockerfile` at `/srv/build-lab/Dockerfile` that:\n - Starts `FROM alpine:3.19`\n - Sets the working directory to `/app`\n - Copies `greeting.txt` into the image\n - Runs `cat greeting.txt` as its default command\n2. Build it, tagged `rootnode-build-lab:1`\n3. Run a container from YOUR image and confirm it prints the real content of `greeting.txt`", + "hints": [ + { + "title": "The Dockerfile", + "body": "Each instruction builds one layer: FROM sets the base, WORKDIR sets the working directory for everything after it, COPY brings a file from the build context into the image, CMD sets the default command run when a container starts.", + "command": "cat < /srv/build-lab/Dockerfile\nFROM alpine:3.19\nWORKDIR /app\nCOPY greeting.txt .\nCMD cat greeting.txt\nEOF" + }, + { + "title": "Building and running", + "body": "`docker build -t ` builds using the Dockerfile found in that directory. `docker run --rm ` runs it once and cleans up automatically afterward.", + "command": "cd /srv/build-lab && docker build -t rootnode-build-lab:1 .\ndocker run --rm rootnode-build-lab:1" + } + ], + "setup_commands": [ + { "command": "rm -rf /srv/build-lab && mkdir -p /srv/build-lab" }, + { "command": "echo 'Hello from my custom image' > /srv/build-lab/greeting.txt" }, + { "command": "docker pull alpine:3.19" } + ], + "validation": { + "commands": [ + { "description": "the image was actually built with the expected tag", "command": "docker image inspect rootnode-build-lab:1 --format '{{.RepoTags}}' 2>/dev/null", "expected_output": "rootnode-build-lab:1", "match": "contains" }, + { "description": "a container from the student's own image produces the real file content", "command": "docker run --rm rootnode-build-lab:1 2>/dev/null", "expected_output": "Hello from my custom image", "match": "exact" } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { "command": "docker rmi rootnode-build-lab:1 2>/dev/null; rm -rf /srv/build-lab" } + ] +} diff --git a/scenarios/data/docker-cli-basics.json b/scenarios/data/docker-cli-basics.json new file mode 100644 index 0000000..89716f4 --- /dev/null +++ b/scenarios/data/docker-cli-basics.json @@ -0,0 +1,38 @@ +{ + "id": "docker-cli-basics", + "title": "Run, Exec Into, and Inspect a Real Container", + "category": "Containers", + "isolation_type": "shared_namespace", + "difficulty": "Easy", + "type": "task", + "weight": 5, + "description": "## Run, Exec Into, and Inspect a Real Container\n\n**Your task:**\n\n1. Run a container named `cli-lab-app` from the `nginx:alpine` image, in the background (detached)\n2. Use `docker exec` to run a command INSIDE the running container that creates `/tmp/marker.txt` containing exactly `hello-from-exec`\n3. Record the running container's ID to `/srv/docker-cli-lab/container-id.txt`\n4. Use `docker logs` and `docker inspect` to confirm it's actually running — you don't need to write those findings anywhere, just use them to verify your own work", + "hints": [ + { + "title": "Running and execing", + "body": "`docker run -d --name ` starts a container in the background under a name you choose instead of a random one. `docker exec ` runs a NEW command inside an ALREADY-running container — different from `docker run`, which starts a brand new container.", + "command": "docker run -d --name cli-lab-app nginx:alpine\ndocker exec cli-lab-app sh -c 'echo hello-from-exec > /tmp/marker.txt'" + }, + { + "title": "Recording the container ID", + "body": "`docker ps -q -f name=` prints just the ID of a container matching that name filter.", + "command": "mkdir -p /srv/docker-cli-lab\ndocker ps -q -f name=cli-lab-app > /srv/docker-cli-lab/container-id.txt" + } + ], + "setup_commands": [ + { "command": "mkdir -p /srv/docker-cli-lab" }, + { "command": "docker rm -f cli-lab-app 2>/dev/null; true" }, + { "command": "docker pull nginx:alpine" } + ], + "validation": { + "commands": [ + { "description": "cli-lab-app is running from the correct image", "command": "docker inspect cli-lab-app --format '{{.Config.Image}} {{.State.Running}}' 2>/dev/null", "expected_output": "nginx:alpine true", "match": "exact" }, + { "description": "the marker file was actually created inside the container via exec", "command": "docker exec cli-lab-app cat /tmp/marker.txt 2>/dev/null", "expected_output": "hello-from-exec", "match": "exact" }, + { "description": "the container ID was correctly recorded and matches the real running container", "command": "[ \"$(cat /srv/docker-cli-lab/container-id.txt 2>/dev/null)\" = \"$(docker ps -q -f name=cli-lab-app)\" ] && echo match || echo mismatch", "expected_output": "match", "match": "exact" } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { "command": "docker rm -f cli-lab-app 2>/dev/null; rm -rf /srv/docker-cli-lab" } + ] +} diff --git a/scenarios/data/docker-compose-multiservice.json b/scenarios/data/docker-compose-multiservice.json new file mode 100644 index 0000000..e714c81 --- /dev/null +++ b/scenarios/data/docker-compose-multiservice.json @@ -0,0 +1,38 @@ +{ + "id": "docker-compose-multiservice", + "title": "Run a Real Two-Service Application With Compose", + "category": "Containers", + "isolation_type": "shared_namespace", + "difficulty": "Medium", + "type": "task", + "weight": 7, + "description": "## Run a Real Two-Service Application With Compose\n\nManually running two related containers, wiring their network, and keeping track of dependency order gets tedious fast — Compose describes the whole application in one file.\n\n**Your task**, at `/srv/compose-lab/docker-compose.yml`:\n\n1. Define a `web` service from `nginx:alpine`, publishing host port `8077` to container port `80`\n2. Define an `api` service from `alpine:3.19`, running `sh -c 'echo api-ready > /tmp/ready && sleep 3600'`\n3. Make `web` depend on `api` (`depends_on`)\n4. Bring the whole application up with `docker compose up -d`\n5. Confirm both services are actually running, and that `curl http://localhost:8077/` succeeds", + "hints": [ + { + "title": "The compose file", + "body": "A `services:` block defines each container as a named service — `ports` publishes a port exactly like `docker run -p`, `depends_on` controls startup order.", + "command": "cat < /srv/compose-lab/docker-compose.yml\nservices:\n web:\n image: nginx:alpine\n ports:\n - '8077:80'\n depends_on:\n - api\n api:\n image: alpine:3.19\n command: sh -c 'echo api-ready > /tmp/ready && sleep 3600'\nEOF" + }, + { + "title": "Bringing it up", + "body": "`docker compose up -d` reads the compose file in the current directory (or use `-f` to point at one elsewhere) and starts every service defined in it.", + "command": "cd /srv/compose-lab && docker compose up -d" + } + ], + "setup_commands": [ + { "command": "rm -rf /srv/compose-lab && mkdir -p /srv/compose-lab" }, + { "command": "docker pull nginx:alpine" }, + { "command": "docker pull alpine:3.19" } + ], + "validation": { + "commands": [ + { "description": "the web service is running", "command": "docker compose -f /srv/compose-lab/docker-compose.yml ps --format '{{.Service}}:{{.State}}' 2>/dev/null | grep web", "expected_output": "web:running", "match": "exact" }, + { "description": "the api service is running", "command": "docker compose -f /srv/compose-lab/docker-compose.yml ps --format '{{.Service}}:{{.State}}' 2>/dev/null | grep api", "expected_output": "api:running", "match": "exact" }, + { "description": "the published port actually works", "command": "curl -sf http://localhost:8077/ -o /dev/null -w '%{http_code}'", "expected_output": "200", "match": "exact" } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { "command": "docker compose -f /srv/compose-lab/docker-compose.yml down 2>/dev/null; rm -rf /srv/compose-lab" } + ] +} diff --git a/scenarios/data/docker-container-networking.json b/scenarios/data/docker-container-networking.json new file mode 100644 index 0000000..b7833bb --- /dev/null +++ b/scenarios/data/docker-container-networking.json @@ -0,0 +1,38 @@ +{ + "id": "docker-container-networking", + "title": "It Works Inside the Container, But Not From the Host — Why?", + "category": "Containers", + "isolation_type": "shared_namespace", + "difficulty": "Medium", + "type": "task", + "weight": 6, + "description": "## It Works Inside the Container, But Not From the Host — Why?\n\nA container named `net-lab-web` is already running `nginx:alpine`. It works perfectly INSIDE itself, but `curl http://localhost:8099/` from this Lab's own shell fails.\n\n**Your task:**\n\n1. Confirm the app really does work internally: `docker exec net-lab-web wget -qO- http://localhost:80/`\n2. Confirm `curl http://localhost:8099/` from the host really does fail\n3. Diagnose why (hint: `docker port net-lab-web` shows what ports are actually published — or aren't)\n4. Fix it: remove the container and recreate it WITH the correct port publish flag, so `curl http://localhost:8099/` succeeds", + "hints": [ + { + "title": "Checking what's actually published", + "body": "`docker port ` lists every host-port-to-container-port mapping currently configured — an empty result means NOTHING is published, no matter how well the app works internally.", + "command": "docker port net-lab-web" + }, + { + "title": "The fix — port publishing can't be added to a running container", + "body": "Port publishing is set at container CREATION time, not changeable afterward — you must remove and recreate it with `-p hostPort:containerPort`.", + "command": "docker rm -f net-lab-web\ndocker run -d --name net-lab-web -p 8099:80 nginx:alpine" + } + ], + "setup_commands": [ + { "command": "docker rm -f net-lab-web 2>/dev/null; true" }, + { "command": "docker pull nginx:alpine" }, + { "command": "docker run -d --name net-lab-web nginx:alpine" } + ], + "validation": { + "commands": [ + { "description": "net-lab-web is running from the correct image", "command": "docker inspect net-lab-web --format '{{.Config.Image}} {{.State.Running}}' 2>/dev/null", "expected_output": "nginx:alpine true", "match": "exact" }, + { "description": "port 8099 is now genuinely published to the container's port 80", "command": "docker port net-lab-web 2>/dev/null | grep 80/tcp", "expected_output": "8099", "match": "contains" }, + { "description": "the published port is actually reachable from the host", "command": "curl -sf http://localhost:8099/ -o /dev/null -w '%{http_code}'", "expected_output": "200", "match": "exact" } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { "command": "docker rm -f net-lab-web 2>/dev/null" } + ] +} diff --git a/scenarios/data/docker-container-storage.json b/scenarios/data/docker-container-storage.json new file mode 100644 index 0000000..545f012 --- /dev/null +++ b/scenarios/data/docker-container-storage.json @@ -0,0 +1,37 @@ +{ + "id": "docker-container-storage", + "title": "Prove Data Survives a Container's Death — With a Volume", + "category": "Containers", + "isolation_type": "shared_namespace", + "difficulty": "Medium", + "type": "task", + "weight": 6, + "description": "## Prove Data Survives a Container's Death — With a Volume\n\nA container's own writable layer is disposable — data written there disappears when the container is destroyed. A named VOLUME is different.\n\n**Your task:**\n\n1. Create a named volume: `docker volume create storage-lab-vol`\n2. Run a container named `storage-lab-app` from `alpine:3.19` (keep it running with `sleep 3600`), mounting the volume at `/data`\n3. Write a file `/data/file.txt` containing exactly `persisted-data`, inside the container\n4. Destroy the container completely (`docker rm -f storage-lab-app`)\n5. Recreate it with the SAME name, SAME image, and the SAME volume mount\n6. Confirm `/data/file.txt` still contains `persisted-data` — proving the volume, not the container, is what actually held the data", + "hints": [ + { + "title": "Creating and mounting a volume", + "body": "`-v :` mounts a named volume at a path inside the container — data written there lives in the volume, independent of the container's own lifecycle.", + "command": "docker volume create storage-lab-vol\ndocker run -d --name storage-lab-app -v storage-lab-vol:/data alpine:3.19 sleep 3600\ndocker exec storage-lab-app sh -c 'echo persisted-data > /data/file.txt'" + }, + { + "title": "Destroying and recreating", + "body": "Removing the container does NOT remove the volume — recreating a container with the same `-v` flag reattaches the exact same data.", + "command": "docker rm -f storage-lab-app\ndocker run -d --name storage-lab-app -v storage-lab-vol:/data alpine:3.19 sleep 3600\ndocker exec storage-lab-app cat /data/file.txt" + } + ], + "setup_commands": [ + { "command": "docker rm -f storage-lab-app 2>/dev/null; docker volume rm storage-lab-vol 2>/dev/null; true" }, + { "command": "docker pull alpine:3.19" } + ], + "validation": { + "commands": [ + { "description": "storage-lab-vol volume exists", "command": "docker volume inspect storage-lab-vol --format '{{.Name}}' 2>/dev/null", "expected_output": "storage-lab-vol", "match": "exact" }, + { "description": "storage-lab-app is running and mounts the volume at /data", "command": "docker inspect storage-lab-app --format '{{range .Mounts}}{{.Name}}:{{.Destination}} {{end}}' 2>/dev/null", "expected_output": "storage-lab-vol:/data", "match": "contains" }, + { "description": "the data genuinely survived a container destroy/recreate cycle", "command": "docker exec storage-lab-app cat /data/file.txt 2>/dev/null", "expected_output": "persisted-data", "match": "exact" } + ] + }, + "default_namespace": "default", + "teardown_commands": [ + { "command": "docker rm -f storage-lab-app 2>/dev/null; docker volume rm storage-lab-vol 2>/dev/null; true" } + ] +} diff --git a/scenarios/data/docker-image-vs-container-mcq.json b/scenarios/data/docker-image-vs-container-mcq.json new file mode 100644 index 0000000..adb2344 --- /dev/null +++ b/scenarios/data/docker-image-vs-container-mcq.json @@ -0,0 +1,22 @@ +{ + "id": "docker-image-vs-container-mcq", + "title": "Image vs. Container — The Distinction That Confuses Everyone at First", + "category": "Containers", + "isolation_type": "shared_namespace", + "difficulty": "Easy", + "type": "mcq", + "weight": 3, + "description": "## Image vs. Container — The Distinction That Confuses Everyone at First\n\nYou run `docker run nginx:alpine` three separate times without removing anything in between. How many containers now exist, and how many images?", + "options": [ + { "id": "a", "text": "3 containers, 3 images — each run creates its own independent image" }, + { "id": "b", "text": "3 containers, 1 image — the image is the packaged artifact pulled once; each `run` creates a new, independent RUNNING INSTANCE from it" }, + { "id": "c", "text": "1 container, 1 image — running the same image twice just reuses the same container" }, + { "id": "d", "text": "1 container, 3 images — Docker versions the image automatically on each run" } + ], + "correct_option": "b", + "explanation": "An image is packaged, read-only artifact — pulling `nginx:alpine` downloads it exactly once. A container is a RUNNING INSTANCE of an image; running the same image multiple times creates multiple independent containers (with their own writable layer, network identity, and lifecycle), all sharing the same underlying image.", + "hints": [], + "setup_commands": [], + "default_namespace": "default", + "teardown_commands": [] +} diff --git a/scenarios/data/docker-port-publish-mcq.json b/scenarios/data/docker-port-publish-mcq.json new file mode 100644 index 0000000..4dbb581 --- /dev/null +++ b/scenarios/data/docker-port-publish-mcq.json @@ -0,0 +1,22 @@ +{ + "id": "docker-port-publish-mcq", + "title": "Works Inside, Unreachable Outside", + "category": "Containers", + "isolation_type": "shared_namespace", + "difficulty": "Medium", + "type": "mcq", + "weight": 4, + "description": "## Works Inside, Unreachable Outside\n\nA web server container is confirmed healthy — `docker exec curl localhost:80` succeeds from inside it. But `curl http://localhost:8080/` from the HOST fails to connect. The container was started with plain `docker run -d myapp` (no other flags). What's most likely missing?", + "options": [ + { "id": "a", "text": "The image itself is broken" }, + { "id": "b", "text": "A `-p 8080:80` port-publish flag was never set when the container was created, so nothing on the host maps to the container's port 80" }, + { "id": "c", "text": "The container needs more memory" }, + { "id": "d", "text": "DNS is misconfigured" } + ], + "correct_option": "b", + "explanation": "Working correctly INSIDE the container proves the application itself is fine — the failure is specifically that the host has no mapping to the container's internal port. Port publishing (`-p hostPort:containerPort`) must be set at container creation time; without it, the container's ports are only reachable from other containers on the same Docker network, never from the host.", + "hints": [], + "setup_commands": [], + "default_namespace": "default", + "teardown_commands": [] +}