feat(root04): author Containers & Docker
Adds all 8 ROOT 04 modules (why containers exist, images & containers, Docker CLI, container networking, container storage, Dockerfile & image build, Docker Compose, containers → Kubernetes) — 43 activities, using the nested Docker Engine Phase 6D built and tested. Modules 1, 2, and 8 are conceptual (no lab by design) — the honest bridge module (M8) explicitly names what Kubernetes adds beyond what this course already taught, without re-teaching Kubernetes itself. Adds 5 new hands-on Docker scenarios plus 2 MCQs, all against the real nested Docker Engine: docker-cli-basics (run/exec/inspect a real container), docker-container-networking (a genuine "works internally, unreachable from host" port-publishing failure, diagnosed and fixed by recreating the container with the correct `-p` flag), docker-container-storage (a real destroy/recreate cycle proving named-volume persistence, not just described), docker-build-image (a real Dockerfile, a real build, a real custom image run), and docker-compose-multiservice (a real two-service Compose application with a real published port). All tested end-to-end via the real scenario-validation API, including negative tests. Wires rootnode-containers-assessment (2 required + 4 random-draw scenarios) using the existing Phase 3B assessment engine. This completes the entire ROOT_ Foundations sequence (ROOT 01-04) — see docs/PHASE6E_ROOT_FOUNDATIONS_COMPLETION.md. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
+51
@@ -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.)
|
||||
+49
@@ -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.
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Knowledge Check: Why Containers Exist
|
||||
|
||||
---
|
||||
|
||||
**1. What real problem does "it works on my machine" name?**
|
||||
|
||||
*Answer:* Dependency/environment inconsistency between where an application was developed and
|
||||
where it actually needs to run — differences in libraries, versions, and configuration that are
|
||||
easy to overlook and hard to fully document by hand.
|
||||
|
||||
---
|
||||
|
||||
**2. What's the actual architectural difference between a container and a virtual machine, not
|
||||
just "containers are smaller"?**
|
||||
|
||||
*Answer:* A container shares the host machine's kernel and only isolates the application's
|
||||
process/filesystem/network view. A VM virtualizes an entire computer, including running its own
|
||||
separate kernel — a genuinely different isolation model, not just a size difference.
|
||||
|
||||
---
|
||||
|
||||
**3. Why do containers start so much faster than VMs?**
|
||||
|
||||
*Answer:* A container doesn't boot a separate operating system/kernel — it starts a process
|
||||
using the ALREADY-RUNNING host kernel, while a VM has to boot an entire independent OS from
|
||||
scratch.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Question 2 is the one worth confirming explicitly — many students carry the
|
||||
misconception "container = tiny VM" past this point without correction.
|
||||
|
||||
**Approximate timing:** 5 minutes.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Reflection: You Now Know What Problem You're About to Solve, Hands-On
|
||||
|
||||
## Reflection questions
|
||||
|
||||
1. Think back to ROOT 01's "how computers run software" module. How does today's lesson refine
|
||||
your understanding of what a "process" actually is?
|
||||
2. Do you expect containers to make the Linux/process/networking skills from ROOT 01-02 less
|
||||
relevant, or MORE relevant? Why?
|
||||
|
||||
## Real Ops takeaway
|
||||
|
||||
**Every skill from ROOT 01-02 — processes, filesystems, networking — transfers directly into
|
||||
containers, because a container is built from exactly those same primitives, just isolated
|
||||
differently.** You are not about to learn an unrelated new subject; you're about to see the
|
||||
Linux fundamentals you already have applied through Docker's specific tooling. Module 2 starts
|
||||
that translation.
|
||||
|
||||
This module is complete. Continue to the next module.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Discussion questions for a live classroom:** question 2's honest answer is "more relevant" —
|
||||
worth stating explicitly, since students sometimes assume containers are an alternative to
|
||||
Linux fundamentals rather than built directly on top of them.
|
||||
|
||||
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
# Images and Containers
|
||||
|
||||
## The single most important distinction in this course
|
||||
|
||||
**An image is a packaged, read-only ARTIFACT. A container is a RUNNING INSTANCE of an image.**
|
||||
This is exactly analogous to a program's executable file (the image) versus a running process
|
||||
of that program (the container) — a distinction ROOT 01's process module already gave you
|
||||
vocabulary for. You can run the SAME image many times, producing many independent containers,
|
||||
the same way you can run the same program multiple times as separate processes.
|
||||
|
||||
## Layers: how an image is actually built
|
||||
|
||||
An image is built from a stack of **layers**, each representing one change (install a package,
|
||||
copy in application code, set a configuration). Layers are cached and shared — if two images
|
||||
both start `FROM alpine:3.19`, they share that base layer on disk rather than duplicating it,
|
||||
which is why pulling a new image that shares layers with one you already have is often much
|
||||
faster than the total image size suggests.
|
||||
|
||||
## Registries, repositories, and tags
|
||||
|
||||
- **Registry** — a server that stores and distributes images (Docker Hub is the default public
|
||||
one; a company might run a private registry).
|
||||
- **Repository** — a named collection of related images (e.g. `nginx`).
|
||||
- **Tag** — a specific version within a repository (`nginx:alpine`, `nginx:1.25`, `nginx:latest`).
|
||||
`latest` is just a conventional tag name, not automatically "the newest" in any enforced sense
|
||||
— relying on it in production is a common, avoidable source of surprise.
|
||||
|
||||
## The core commands, mapped to the model
|
||||
|
||||
- **`docker pull <image>`** — download an image from a registry, without running it.
|
||||
- **`docker run <image>`** — create and start a NEW container from an image (pulls automatically
|
||||
if not already present locally).
|
||||
- **`docker stop <container>`** — gracefully stop a running container (its process, not the
|
||||
image).
|
||||
- **`docker rm <container>`** — delete a stopped container. This does NOT delete the image it
|
||||
was created from.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Lesson goal:** Land the image/container distinction as concretely as possible — nearly every
|
||||
later confusion in this course traces back to conflating the two.
|
||||
|
||||
**Approximate timing:** 15 minutes read; 10 minutes live.
|
||||
|
||||
**Common misconception to watch for:** using "image" and "container" interchangeably in casual
|
||||
speech — gently correct this early and often, since precise vocabulary here prevents real
|
||||
confusion later (e.g., "did you delete the container or the image?" matters).
|
||||
|
||||
**Discussion question:** "If you run `docker run nginx:alpine` three times, how many images
|
||||
exist afterward? How many containers?" (Answer: 1 image, 3 containers — a good check before
|
||||
moving to the hands-on Lab.)
|
||||
+59
@@ -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.
|
||||
+38
@@ -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.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Guided Exercise: Trace the Counts Through a Sequence of Commands
|
||||
|
||||
This is a paper exercise — commit to your answer before checking it in the Lab.
|
||||
|
||||
## Your task
|
||||
|
||||
Starting from a completely clean system (no images, no containers), predict the number of
|
||||
IMAGES and the number of CONTAINERS after each step:
|
||||
|
||||
1. `docker pull redis:alpine`
|
||||
2. `docker run -d --name cache1 redis:alpine`
|
||||
3. `docker run -d --name cache2 redis:alpine`
|
||||
4. `docker rm -f cache1`
|
||||
5. `docker rmi redis:alpine` (attempted while `cache2` is still running)
|
||||
|
||||
## A worked shape to check yourself against
|
||||
|
||||
1. 1 image, 0 containers.
|
||||
2. 1 image, 1 container.
|
||||
3. 1 image, 2 containers.
|
||||
4. 1 image, 1 container (only `cache1` removed).
|
||||
5. This actually FAILS — Docker refuses to remove an image that a running container (`cache2`)
|
||||
still depends on, specifically to prevent exactly this kind of accident. Still 1 image, 1
|
||||
container, plus an error message.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Item 5 is the most valuable one — many students haven't seen Docker's
|
||||
dependency-protection behavior yet, and it's a good real safety feature to highlight.
|
||||
|
||||
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Reflection: A Small Distinction With Large Consequences
|
||||
|
||||
## Reflection questions
|
||||
|
||||
1. Before this module, would you have known why `docker rm` and `docker rmi` are two separate
|
||||
commands? Does it make sense now?
|
||||
2. Where else in ROOT_ have you seen this exact "artifact vs. running instance" pattern before?
|
||||
(Hint: think back to ROOT 01.)
|
||||
|
||||
## Real Ops takeaway
|
||||
|
||||
**Precise vocabulary prevents real mistakes.** Confusing "stop the container" with "delete the
|
||||
image," or assuming `latest` means "the current version," are genuine, common sources of
|
||||
production incidents — not just semantic nitpicking. The image/container distinction you just
|
||||
solidified is the foundation every command in the rest of this course builds on.
|
||||
|
||||
This module is complete. Continue to the next module.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Discussion questions for a live classroom:** question 2's intended answer is "program vs.
|
||||
process," from ROOT 01 — a good moment to explicitly connect the two courses.
|
||||
|
||||
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Docker CLI
|
||||
|
||||
## The commands you'll use constantly
|
||||
|
||||
Module 2 introduced pull/run/stop/rm conceptually. This module adds the rest of the everyday
|
||||
toolkit — and every one of them maps directly to a Linux concept you already have from ROOT 01.
|
||||
|
||||
## `docker ps` — what's running right now
|
||||
|
||||
```
|
||||
$ docker ps
|
||||
CONTAINER ID IMAGE STATUS NAMES
|
||||
a1b2c3d4e5f6 nginx:alpine Up 2 minutes web
|
||||
```
|
||||
|
||||
This is the container-level equivalent of ROOT 02's `ps aux` — "what's actually running." Add
|
||||
`-a` to see stopped containers too, not just running ones.
|
||||
|
||||
## `docker logs` — what has it been saying?
|
||||
|
||||
```
|
||||
$ docker logs web
|
||||
```
|
||||
|
||||
The container-level equivalent of reading a service's log file from ROOT 02 — except Docker
|
||||
captures a container's stdout/stderr automatically, so there's no config needed to find WHERE
|
||||
the logs live, unlike a traditional service.
|
||||
|
||||
## `docker exec` — run a NEW command inside an ALREADY-running container
|
||||
|
||||
```
|
||||
$ docker exec web sh -c 'cat /etc/nginx/nginx.conf'
|
||||
```
|
||||
|
||||
Different from `docker run`, which creates a brand-new container — `exec` reaches INTO an
|
||||
existing, running one, exactly like ROOT 01's `ps`/inspection commands but scoped to one
|
||||
specific container's process namespace.
|
||||
|
||||
## `docker inspect` — the full, detailed truth about a container
|
||||
|
||||
```
|
||||
$ docker inspect web --format '{{.State.Running}}'
|
||||
true
|
||||
```
|
||||
|
||||
Returns a large JSON document with everything Docker knows about a container — `--format` lets
|
||||
you extract just the one field you care about, rather than reading the whole thing.
|
||||
|
||||
## `docker stop` vs `docker rm`
|
||||
|
||||
`docker stop` gracefully stops a running container (sends SIGTERM, then SIGKILL after a grace
|
||||
period — literally ROOT 01's process-signals lesson, applied to a container's main process).
|
||||
`docker rm` deletes a STOPPED container entirely — `docker rm -f` combines both (force-stop then
|
||||
remove) in one step.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Lesson goal:** Explicitly map every Docker CLI command back to the equivalent ROOT 01/02
|
||||
Linux concept students already have — this is a translation exercise, not new material.
|
||||
|
||||
**Approximate timing:** 15 minutes read; 10 minutes live.
|
||||
|
||||
**Common misconception to watch for:** confusing `docker run` (creates a new container) with
|
||||
`docker exec` (runs a command in an existing one) — a very common early mixup.
|
||||
|
||||
**Demo idea:** run `docker exec` against a container, then immediately `docker run` the same
|
||||
image again, and compare `docker ps` before/after to show the difference concretely.
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
# Demonstration: `run` Creates, `exec` Reaches In
|
||||
|
||||
## OBSERVE
|
||||
|
||||
```
|
||||
$ docker run -d --name web nginx:alpine
|
||||
$ docker ps --format '{{.Names}}'
|
||||
web
|
||||
```
|
||||
|
||||
One container exists.
|
||||
|
||||
## HYPOTHESIS
|
||||
|
||||
Predict: will `docker exec web echo hello` create a SECOND container?
|
||||
|
||||
## INSPECT
|
||||
|
||||
```
|
||||
$ docker exec web echo hello
|
||||
hello
|
||||
$ docker ps --format '{{.Names}}'
|
||||
web
|
||||
```
|
||||
|
||||
Still exactly one container — `exec` ran a new command INSIDE the existing one, it never
|
||||
created anything new.
|
||||
|
||||
## CHANGE
|
||||
|
||||
Now actually run the same image again:
|
||||
|
||||
```
|
||||
$ docker run -d --name web2 nginx:alpine
|
||||
$ docker ps --format '{{.Names}}'
|
||||
web2
|
||||
web
|
||||
```
|
||||
|
||||
Now two containers exist — because `run`, unlike `exec`, always creates a NEW container.
|
||||
|
||||
## VERIFY
|
||||
|
||||
```
|
||||
$ docker logs web | tail -3
|
||||
$ docker inspect web --format '{{.State.Running}}'
|
||||
true
|
||||
```
|
||||
|
||||
Both containers are independent, each with their own logs and state — exactly as Module 2's
|
||||
"one image, many containers" demonstration showed.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Demo goal:** Directly contrast `run` and `exec`'s effect on the container COUNT — this is the
|
||||
most common early confusion and deserves a side-by-side comparison.
|
||||
|
||||
**Approximate timing:** 8 minutes live.
|
||||
|
||||
**Common mistake to watch for:** trying to `exec` into a container that isn't running yet
|
||||
(`exec` requires an ALREADY-running target) — a good moment to connect back to `docker ps`
|
||||
first.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Knowledge Check: Docker CLI
|
||||
|
||||
---
|
||||
|
||||
**1. What's the real difference between `docker run` and `docker exec`?**
|
||||
|
||||
*Answer:* `docker run` always creates a brand-new container from an image. `docker exec` runs a
|
||||
new command inside an ALREADY-running container — no new container is created.
|
||||
|
||||
---
|
||||
|
||||
**2. Which command is the container-level equivalent of ROOT 02's "what's running" question
|
||||
(`ps aux` / service inspection)?**
|
||||
|
||||
*Answer:* `docker ps` (add `-a` to include stopped containers too).
|
||||
|
||||
---
|
||||
|
||||
**3. Why doesn't `docker logs` require you to know a specific log file path, unlike a
|
||||
traditional Linux service?**
|
||||
|
||||
*Answer:* Docker automatically captures a container's stdout/stderr output — there's no
|
||||
separate log file configuration needed the way ROOT 02's services required.
|
||||
|
||||
---
|
||||
|
||||
**4. What does `docker stop` actually send to a container's main process, and why does that
|
||||
matter?**
|
||||
|
||||
*Answer:* SIGTERM first (a polite shutdown request, giving the process a chance to clean up),
|
||||
then SIGKILL after a grace period if it hasn't exited — the exact same signal discipline ROOT 01
|
||||
taught for regular Linux processes, applied here to a container's main process.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Question 1 is the single most important check in this module — revisit the
|
||||
demo if students get it wrong.
|
||||
|
||||
**Approximate timing:** 5 minutes.
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Guided Exercise: Which Docker Command Actually Does This?
|
||||
|
||||
This is a paper exercise — commit to an answer before checking it in the Lab.
|
||||
|
||||
## Your task
|
||||
|
||||
For each goal, name the ONE Docker command you'd use:
|
||||
|
||||
1. "See every container, including ones that have stopped."
|
||||
2. "Run a one-off diagnostic command inside a container that's already running."
|
||||
3. "Read what a container has printed to its logs so far."
|
||||
4. "Check the exact value of one specific field (like whether it's currently running) without
|
||||
reading the entire inspect output."
|
||||
5. "Completely remove a running container in one step, without stopping it first separately."
|
||||
|
||||
## A worked shape to check yourself against
|
||||
|
||||
1. `docker ps -a`
|
||||
2. `docker exec <container> <command>`
|
||||
3. `docker logs <container>`
|
||||
4. `docker inspect <container> --format '{{...}}'`
|
||||
5. `docker rm -f <container>`
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Have students justify why each answer fits the described GOAL specifically,
|
||||
not just recite familiar syntax.
|
||||
|
||||
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Reflection: The Whole Toolkit Was Already Familiar
|
||||
|
||||
## Reflection questions
|
||||
|
||||
1. Which Docker command in this module felt like "a new version of something I already knew"
|
||||
from ROOT 01 or ROOT 02? Which felt genuinely new?
|
||||
2. You ran, inspected, execed into, and removed a real container this module. What's the ONE
|
||||
command you'd reach for first if a container "seemed broken" and you didn't know why yet?
|
||||
|
||||
## Real Ops takeaway
|
||||
|
||||
**Every Docker command in this module is a direct translation of a Linux skill you already
|
||||
had.** `docker ps` is `ps aux`. `docker logs` is reading a log file, minus the config. `docker
|
||||
exec` is reaching into a running process. This is exactly why ROOT 01-02 came before ROOT 04 —
|
||||
containers don't replace Linux fundamentals, they wrap them in a specific, consistent interface.
|
||||
|
||||
This module is complete. Continue to the next module.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Discussion questions for a live classroom:** question 2's honest answer is usually `docker ps`
|
||||
or `docker logs` — a good moment to connect back to ROOT 02's five-question service checklist.
|
||||
|
||||
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
# Container Networking
|
||||
|
||||
## This module is ROOT 02's networking model, one layer up
|
||||
|
||||
Every concept here is a direct application of ROOT 02 Module 3's host-to-application chain —
|
||||
just with a container standing in for "host."
|
||||
|
||||
## Every container gets its own IP
|
||||
|
||||
By default, Docker gives each container its own IP address on a private, internal Docker
|
||||
network — exactly like a real host has an IP on a real network (ROOT 02 Module 3). Containers on
|
||||
the SAME Docker network can reach each other directly by that IP.
|
||||
|
||||
## Container-to-container DNS: reach a container by NAME
|
||||
|
||||
Docker runs its own internal DNS for user-defined networks — containers on the same network can
|
||||
resolve each other by CONTAINER NAME, not just IP. If a `web` container needs to reach an `api`
|
||||
container, it can simply connect to hostname `api` — Docker resolves it automatically. This is
|
||||
the exact "getent hosts" concept from ROOT 02, just automated for you.
|
||||
|
||||
## Publishing a port: the host/container boundary
|
||||
|
||||
A container's ports are, by default, only reachable from OTHER containers on the same Docker
|
||||
network — NOT from the host machine, and NOT from outside. **Publishing** a port
|
||||
(`-p hostPort:containerPort`) explicitly creates a mapping: traffic arriving at the HOST's
|
||||
`hostPort` gets forwarded to the CONTAINER's `containerPort`. This is set at container CREATION
|
||||
time — you cannot add it to an already-running container; it must be recreated.
|
||||
|
||||
```
|
||||
docker run -d -p 8080:80 nginx:alpine
|
||||
```
|
||||
|
||||
Host port 8080 → this container's port 80. `curl http://localhost:8080/` from the host now
|
||||
reaches it; `curl http://localhost:80/` from the host does NOT (the host's own port 80 was never
|
||||
touched).
|
||||
|
||||
## A working app that's still unreachable is a real, common failure
|
||||
|
||||
Exactly like ROOT 02's networking-diagnosis modules: a container can be perfectly healthy
|
||||
internally (`docker exec <container> curl localhost:80` succeeds) while being completely
|
||||
unreachable from the host, because publishing was never configured. This is one of the single
|
||||
most common container networking mistakes — and this module's Lab makes you diagnose and fix
|
||||
it for real.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Lesson goal:** Explicitly connect every idea here back to ROOT 02's networking model — this
|
||||
module should feel like recognition, not new material, reinforcing the curriculum's continuity.
|
||||
|
||||
**Approximate timing:** 15 minutes read; 10 minutes live.
|
||||
|
||||
**Common misconception to watch for:** assuming a container's port is automatically reachable
|
||||
from the host just because the container itself works fine — this exact misconception is what
|
||||
this module's Lab is built to correct through direct experience.
|
||||
|
||||
**Discussion question:** "Why might Docker default to NOT exposing container ports to the host
|
||||
automatically?" (Good answer: explicit opt-in is safer — you don't want every container's every
|
||||
port accidentally reachable from outside by default.)
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
# Demonstration: Reaching a Container By Name, Not IP
|
||||
|
||||
## OBSERVE
|
||||
|
||||
```
|
||||
$ docker network create demo-net
|
||||
$ docker run -d --name api-server --network demo-net nginx:alpine
|
||||
```
|
||||
|
||||
## HYPOTHESIS
|
||||
|
||||
Predict: can another container on `demo-net` reach `api-server` using its NAME as a hostname,
|
||||
without knowing its IP?
|
||||
|
||||
## INSPECT
|
||||
|
||||
```
|
||||
$ docker run --rm --network demo-net alpine:3.19 sh -c "wget -qO- http://api-server:80/ | head -1"
|
||||
<!DOCTYPE html>
|
||||
```
|
||||
|
||||
Yes — Docker's built-in DNS resolved `api-server` (a container NAME) directly, no IP address
|
||||
needed anywhere in that command.
|
||||
|
||||
## CHANGE
|
||||
|
||||
Now try reaching it from a container that is NOT on the same network:
|
||||
|
||||
```
|
||||
$ docker run --rm alpine:3.19 sh -c "wget -qO- http://api-server:80/ 2>&1 | head -1"
|
||||
wget: bad address 'api-server'
|
||||
```
|
||||
|
||||
Name resolution fails entirely — the container isn't even on the same Docker network, so there's
|
||||
no DNS entry for it there.
|
||||
|
||||
## VERIFY
|
||||
|
||||
Container-to-container name resolution is scoped to a SPECIFIC Docker network, not global —
|
||||
exactly like ROOT 02's subnet concept: reachability depends on which network you're actually on.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Demo goal:** Show container-name DNS resolution as a real, working feature — and show it
|
||||
explicitly FAILING when containers aren't on the same network, reinforcing that this isn't
|
||||
magic, it's scoped.
|
||||
|
||||
**Approximate timing:** 8 minutes live.
|
||||
|
||||
**Common mistake to watch for:** assuming any two containers can always reach each other by name
|
||||
regardless of network — reinforce the "same network required" rule explicitly.
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# Knowledge Check: Container Networking
|
||||
|
||||
---
|
||||
|
||||
**1. A container works fine when tested with `docker exec <container> curl localhost:80`, but
|
||||
`curl http://localhost:8080/` from the host fails. What's the most likely cause?**
|
||||
|
||||
*Answer:* The container was never started with a port-publish flag (`-p 8080:80`) mapping the
|
||||
host port to the container's port — the app works internally, but nothing connects the host to
|
||||
it.
|
||||
|
||||
---
|
||||
|
||||
**2. Can you add port publishing to an already-running container?**
|
||||
|
||||
*Answer:* No — port publishing is set at container creation time; you must remove and recreate
|
||||
the container with the correct `-p` flag.
|
||||
|
||||
---
|
||||
|
||||
**3. How can one container reach another by name instead of IP?**
|
||||
|
||||
*Answer:* If both containers are on the same user-defined Docker network, Docker's built-in DNS
|
||||
resolves the target container's NAME automatically — no IP address needed.
|
||||
|
||||
---
|
||||
|
||||
**4. Why does container-name resolution fail between two containers that AREN'T on the same
|
||||
Docker network?**
|
||||
|
||||
*Answer:* Docker's per-network DNS only knows about containers actually attached to that
|
||||
specific network — being on a different (or no) network means there's no DNS entry for the
|
||||
target container at all.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Question 1 directly previews this module's Lab — a good moment to build
|
||||
anticipation for it.
|
||||
|
||||
**Approximate timing:** 5 minutes.
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Guided Exercise: Diagnose Before You Recreate Anything
|
||||
|
||||
This is a paper exercise — commit to a diagnostic plan before working in the Lab.
|
||||
|
||||
## Your task
|
||||
|
||||
A container `payments-api` is reported as "unreachable" by a teammate. Before touching anything,
|
||||
write out, in order, the exact commands you'd run to determine WHERE the problem actually is —
|
||||
using this module's and ROOT 02's tools.
|
||||
|
||||
## A worked shape to check yourself against
|
||||
|
||||
```
|
||||
docker ps --filter name=payments-api # is it even running?
|
||||
docker exec payments-api curl -sf localhost:PORT/ # does it work internally?
|
||||
docker port payments-api # is anything actually published?
|
||||
curl -sf http://localhost:PUBLISHED_PORT/ # does the published mapping actually work?
|
||||
```
|
||||
|
||||
This sequence isolates each layer independently — running? working internally? published at
|
||||
all? published mapping actually functional? — exactly the same "isolate one variable at a time"
|
||||
discipline ROOT 02 Module 3 taught, applied here to containers specifically.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** This exercise is deliberately a direct copy of ROOT 02's diagnostic discipline
|
||||
— the value is students recognizing that connection themselves, not being told it explicitly.
|
||||
|
||||
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Reflection: Same Diagnosis, New Layer
|
||||
|
||||
## Reflection questions
|
||||
|
||||
1. How much of this module's Lab did you solve using pure Docker knowledge, versus reasoning
|
||||
you already had from ROOT 02?
|
||||
2. Where do you expect "published port" reasoning to matter again later in ROOT 05+?
|
||||
|
||||
## Real Ops takeaway
|
||||
|
||||
**"Works internally, unreachable externally" is one of the single most common real-world
|
||||
container networking incidents — and you just diagnosed and fixed a real one.** The exact
|
||||
reasoning you used here (confirm internal health, check what's actually published, verify the
|
||||
published mapping works) is the same reasoning you'll use again for Kubernetes Services later —
|
||||
"published port" becomes "Service," but the underlying question (is anything actually routing
|
||||
traffic to this workload?) doesn't change.
|
||||
|
||||
This module is complete. Continue to the next module.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Discussion questions for a live classroom:** question 2 previews Kubernetes Services directly
|
||||
— worth naming that connection explicitly without teaching Services yet.
|
||||
|
||||
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
|
||||
+61
@@ -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.)
|
||||
+63
@@ -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.
|
||||
+39
@@ -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.
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Guided Exercise: Predict What Survives Before You Test It
|
||||
|
||||
This is a paper exercise — commit to predictions before working in the Lab.
|
||||
|
||||
## Your task
|
||||
|
||||
For each scenario, predict whether the data survives a `docker rm -f` followed by recreating a
|
||||
container with the same name and image:
|
||||
|
||||
1. Data written to `/tmp/file.txt` inside a container with no volumes configured.
|
||||
2. Data written to `/data/file.txt`, where `/data` is mounted from a named volume.
|
||||
3. Data written to `/data/file.txt` in scenario 2, but this time you ALSO run
|
||||
`docker volume rm <the-volume>` before recreating the container.
|
||||
|
||||
## A worked shape to check yourself against
|
||||
|
||||
1. Lost — no volume, so it lived only in the disposable writable layer.
|
||||
2. Survives — the volume itself is untouched by the container's destruction, and the new
|
||||
container reattaches to the same volume.
|
||||
3. Lost — the volume itself was explicitly deleted, not just the container. Persistence depends
|
||||
on the VOLUME surviving, not just avoiding container destruction.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Item 3 is the important trick question — students who've internalized
|
||||
"volumes always persist" without qualification should be corrected here: the volume itself can
|
||||
still be deleted deliberately.
|
||||
|
||||
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Reflection: You Proved Persistence, You Didn't Just Trust It
|
||||
|
||||
## Reflection questions
|
||||
|
||||
1. Before this module, would you have assumed container data persists by default? How
|
||||
expensive would that wrong assumption be in a real production incident?
|
||||
2. Why did this module insist on actually DESTROYING a container to test persistence, rather
|
||||
than just describing the concept?
|
||||
|
||||
## Real Ops takeaway
|
||||
|
||||
**"I assumed it would persist" is one of the most expensive assumptions in real operations
|
||||
work.** You didn't just learn that volumes persist data — you personally destroyed a container
|
||||
and confirmed data loss, then destroyed another and confirmed data survival, with the SAME
|
||||
sequence of commands each time except for one deliberate volume configuration. That's the level
|
||||
of confidence "verify, don't assume" is supposed to give you, and it's exactly the discipline
|
||||
ROOT 02's troubleshooting framework already built.
|
||||
|
||||
This module is complete. Continue to the next module.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Discussion questions for a live classroom:** question 1 works well as a real cautionary
|
||||
example — ask if anyone has heard of (or experienced) a real data-loss incident caused by this
|
||||
exact wrong assumption.
|
||||
|
||||
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
# Dockerfile and Image Build
|
||||
|
||||
## A Dockerfile is a recipe for an image
|
||||
|
||||
Every image you've used so far (`nginx:alpine`, `alpine:3.19`) was itself BUILT from a
|
||||
Dockerfile by someone else. This module teaches you to write your own.
|
||||
|
||||
## The core instructions
|
||||
|
||||
- **`FROM <image>`** — every Dockerfile starts by choosing a base image to build on top of.
|
||||
Almost nothing is built completely from scratch.
|
||||
- **`WORKDIR <path>`** — sets the working directory for every instruction that follows it (and
|
||||
for the container's default working directory when it runs).
|
||||
- **`COPY <host-path> <image-path>`** — copies files from your BUILD CONTEXT (the directory you
|
||||
run `docker build` from) into the image.
|
||||
- **`RUN <command>`** — executes a command DURING the build, creating a new layer with its
|
||||
result (installing a package, for example).
|
||||
- **`CMD <command>`** (conceptually) — sets the DEFAULT command a container runs when started
|
||||
from this image, if nothing else is specified.
|
||||
- **`ENTRYPOINT <command>`** (conceptually) — similar to `CMD`, but harder to override at
|
||||
`docker run` time — the two are often combined (`ENTRYPOINT` as the fixed program,
|
||||
`CMD` as its default arguments).
|
||||
|
||||
## Build context and `.dockerignore`
|
||||
|
||||
`docker build -t mytag .` — that final `.` is the **build context**: the directory Docker can
|
||||
see and copy files FROM during the build. A `.dockerignore` file (same idea as `.gitignore` from
|
||||
ROOT 03) excludes files from the build context — keeping large or irrelevant files (like a local
|
||||
`.git` folder or dependency caches) out of your image and speeding up builds.
|
||||
|
||||
## Layers, again
|
||||
|
||||
Each instruction in a Dockerfile creates one layer, cached individually. Docker reuses cached
|
||||
layers for instructions that haven't changed since the last build — which is why the ORDER of
|
||||
instructions matters for build speed: put things that change rarely (like `FROM` and installing
|
||||
dependencies) before things that change often (like copying your actual application code), so a
|
||||
small code change doesn't invalidate expensive earlier layers.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Lesson goal:** Give students enough Dockerfile literacy to write a real, small, working image
|
||||
— not exhaustive coverage of every possible instruction.
|
||||
|
||||
**Approximate timing:** 18 minutes read; 10 minutes live.
|
||||
|
||||
**Common misconception to watch for:** copying application code before installing dependencies
|
||||
— reinforce the "changes-rarely-first" layer-ordering principle with a concrete example.
|
||||
|
||||
**Demo idea:** build the same Dockerfile twice with no changes, showing the second build using
|
||||
fully cached layers (`CACHED` in the output) and finishing almost instantly.
|
||||
+62
@@ -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.
|
||||
+38
@@ -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.
|
||||
+35
@@ -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.
|
||||
+24
@@ -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.
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
# Docker Compose
|
||||
|
||||
## Real applications are rarely just one container
|
||||
|
||||
A web app usually needs a database, maybe a cache, maybe a background worker — several
|
||||
containers that need to be created, networked together, and started in a sensible order. Doing
|
||||
this manually with individual `docker run` commands works, but gets tedious and error-prone fast,
|
||||
especially remembering the exact flags every time.
|
||||
|
||||
## One file, the whole application
|
||||
|
||||
**Docker Compose** describes an entire multi-container application in one YAML file:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8080:80"
|
||||
depends_on:
|
||||
- api
|
||||
api:
|
||||
image: my-api:1
|
||||
```
|
||||
|
||||
- **`services`** — each entry is one container, using the exact same concepts (image, ports,
|
||||
etc.) you already know from plain `docker run`.
|
||||
- **`depends_on`** — controls STARTUP order (api starts before web) — note this ensures order,
|
||||
not that the dependency is fully READY yet, a subtlety real applications often need extra
|
||||
handling for.
|
||||
- **Networking is automatic** — Compose creates a shared network for all services in the file by
|
||||
default, so `web` can reach `api` by name immediately, no manual `docker network create`
|
||||
needed.
|
||||
|
||||
## The commands
|
||||
|
||||
```
|
||||
docker compose up -d # start every service defined in the file, in the background
|
||||
docker compose ps # see the status of every service
|
||||
docker compose logs # see logs from every service, or docker compose logs <service>
|
||||
docker compose down # stop and remove everything Compose created
|
||||
```
|
||||
|
||||
## Compose doesn't replace anything you've learned — it automates it
|
||||
|
||||
Every Compose service maps directly onto a `docker run` command you could write by hand; Compose
|
||||
just describes the whole set declaratively in one file instead of remembering and re-typing
|
||||
several long commands correctly, every time, in the right order.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Lesson goal:** Frame Compose as automation of already-understood concepts, not new magic —
|
||||
students should be able to mentally translate a `services:` block back into individual
|
||||
`docker run` commands.
|
||||
|
||||
**Approximate timing:** 15 minutes read; 10 minutes live.
|
||||
|
||||
**Common misconception to watch for:** treating `depends_on` as "waits until fully healthy" —
|
||||
it only controls START order, not readiness; worth flagging as a real, common source of race
|
||||
conditions in actual production Compose files.
|
||||
|
||||
**Discussion question:** "Why might describing your application declaratively in one file be
|
||||
better than a shell script that runs several `docker run` commands in sequence?" (Good answer:
|
||||
reviewable, versionable, and Compose itself understands the relationships between services,
|
||||
rather than just blindly executing a sequence.)
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# Demonstration: One File, Two Containers, One Network — Automatically
|
||||
|
||||
## OBSERVE
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8080:80"
|
||||
cache:
|
||||
image: redis:alpine
|
||||
```
|
||||
|
||||
## HYPOTHESIS
|
||||
|
||||
Predict: without running `docker network create` yourself, will `web` be able to reach `cache`
|
||||
by name after `docker compose up`?
|
||||
|
||||
## INSPECT
|
||||
|
||||
```
|
||||
$ docker compose up -d
|
||||
$ docker compose exec web sh -c "wget -qO- http://cache:6379/ 2>&1 | head -1"
|
||||
```
|
||||
|
||||
(Redis doesn't speak HTTP, so this specific request looks odd, but the connection ITSELF
|
||||
succeeding or failing tells you whether name resolution and network reachability work — a
|
||||
`Connection refused` here would mean it REACHED `cache` but Redis rejected the protocol
|
||||
mismatch, which is actually proof the network worked.)
|
||||
|
||||
## CHANGE
|
||||
|
||||
```
|
||||
$ docker compose ps
|
||||
NAME IMAGE STATUS
|
||||
web-1 nginx:alpine Up
|
||||
cache-1 redis:alpine Up
|
||||
$ docker network ls | grep compose
|
||||
xyz123 myproject_default bridge
|
||||
```
|
||||
|
||||
Compose created a network automatically, named after the project directory, with no manual step.
|
||||
|
||||
## VERIFY
|
||||
|
||||
```
|
||||
$ docker compose down
|
||||
$ docker network ls | grep compose
|
||||
(nothing)
|
||||
```
|
||||
|
||||
`docker compose down` cleaned up EVERYTHING Compose created — containers AND the network — in
|
||||
one command.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Demo goal:** Show the automatic networking as a real, observable fact (a real network exists,
|
||||
named and scoped to the project) rather than an assumed convenience.
|
||||
|
||||
**Approximate timing:** 8 minutes live.
|
||||
|
||||
**Common mistake to watch for:** forgetting `docker compose down` and accumulating orphaned
|
||||
networks/containers across multiple exercises — a good habit to reinforce explicitly.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Knowledge Check: Docker Compose
|
||||
|
||||
---
|
||||
|
||||
**1. What does `depends_on` actually guarantee — and NOT guarantee?**
|
||||
|
||||
*Answer:* It guarantees START order (the dependency's container is started before the dependent
|
||||
service's). It does NOT guarantee the dependency is actually READY/healthy yet — a real,
|
||||
common source of race conditions in production Compose files.
|
||||
|
||||
---
|
||||
|
||||
**2. Do you need to manually run `docker network create` for services in the same Compose file
|
||||
to reach each other by name?**
|
||||
|
||||
*Answer:* No — Compose automatically creates a shared network for all services defined in the
|
||||
file, and they can reach each other by service name immediately.
|
||||
|
||||
---
|
||||
|
||||
**3. What does `docker compose down` clean up, that plain `docker compose stop` would not?**
|
||||
|
||||
*Answer:* `down` removes the containers AND the network Compose created; `stop` only stops the
|
||||
containers, leaving them (and the network) in place to be restarted later.
|
||||
|
||||
---
|
||||
|
||||
**4. Does Compose introduce any concept that isn't already just automating `docker run`/
|
||||
`docker network` commands you already know?**
|
||||
|
||||
*Answer:* No — every Compose service maps directly onto concepts already covered
|
||||
(image, ports, networking); Compose automates and declaratively describes them, it doesn't add
|
||||
new underlying mechanisms.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** Question 1 is worth real emphasis — it's a genuine, common production
|
||||
mistake worth preventing early.
|
||||
|
||||
**Approximate timing:** 5 minutes.
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# Guided Exercise: Translate a Compose File Back Into Plain `docker run`
|
||||
|
||||
This is a paper exercise — commit to your translation before checking it in the Lab.
|
||||
|
||||
## Your task
|
||||
|
||||
Given this Compose service definition, write the equivalent plain `docker run` command by hand:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8080:80"
|
||||
```
|
||||
|
||||
Then explain: what would you ALSO need to do manually (that Compose does automatically) if two
|
||||
services needed to reach each other by name?
|
||||
|
||||
## A worked shape to check yourself against
|
||||
|
||||
```
|
||||
docker run -d -p 8080:80 nginx:alpine
|
||||
```
|
||||
|
||||
For two services to reach each other by name manually, you'd need to `docker network create` a
|
||||
shared network yourself, then add `--network <that-network>` to EACH container's `docker run`
|
||||
command — exactly the manual step Compose eliminates.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Facilitation:** This exercise is the clearest possible proof that Compose isn't new magic —
|
||||
being able to translate it back to `docker run` confirms genuine understanding, not just
|
||||
familiarity with YAML syntax.
|
||||
|
||||
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Reflection: A Real Multi-Service Application, Described and Running
|
||||
|
||||
## Reflection questions
|
||||
|
||||
1. Before this module, how would you have run two related containers and connected them
|
||||
manually? How does that compare to this module's Lab?
|
||||
2. What does a Compose file's `services:` block remind you of, from ROOT_'s later Kubernetes
|
||||
content — even without knowing Kubernetes syntax yet?
|
||||
|
||||
## Real Ops takeaway
|
||||
|
||||
**You just described and ran a real multi-container application declaratively — the exact same
|
||||
"describe what you want, let the tool make it real" pattern behind every later Kubernetes manifest
|
||||
in this program.** This isn't a coincidence: Compose and Kubernetes both solve "orchestrate
|
||||
multiple containers as one application," at different scales. The habit of thinking in terms of
|
||||
declared services rather than manual step-by-step commands is what ROOT 05 builds on directly.
|
||||
|
||||
This module is complete. Continue to the final module of this course.
|
||||
|
||||
## Instructor Notes
|
||||
|
||||
**Discussion questions for a live classroom:** question 2 previews Kubernetes manifests
|
||||
directly — a good moment to name "declarative description of desired state" as a term students
|
||||
will hear constantly starting in ROOT 05.
|
||||
|
||||
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
|
||||
+64
@@ -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.)
|
||||
+40
@@ -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.
|
||||
+33
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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 <<EOF > /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 <tag> <context-directory>` builds using the Dockerfile found in that directory. `docker run --rm <tag>` 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" }
|
||||
]
|
||||
}
|
||||
@@ -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 <name> <image>` starts a container in the background under a name you choose instead of a random one. `docker exec <name> <command>` 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=<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" }
|
||||
]
|
||||
}
|
||||
@@ -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 <<EOF > /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" }
|
||||
]
|
||||
}
|
||||
@@ -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 <container>` 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" }
|
||||
]
|
||||
}
|
||||
@@ -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 <volume-name>:<path-in-container>` 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" }
|
||||
]
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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 <container> 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": []
|
||||
}
|
||||
Reference in New Issue
Block a user