33 Commits
Author SHA1 Message Date
ced4568 e6a5902ddf docs(website): record that the website-side registration/checkout flow is now built
ced4568/RootNode-academy-website (separate GitHub repo, Chase-authorized access) now implements
docs/WEBSITE_REGISTRATION_AND_CHECKOUT.md's full contract on its own side: public offering
lookup, registration (adult + guardian/minor), agreement acceptance, checkout creation, and a
status-polling success page. Its own stale pricing was also corrected to match docs/
ROOTNODE_PRICING_AND_OFFERS_V1.md. It has nothing to talk to yet -- no real Platform API exists
(Gate 4's hardware blocker, unchanged) -- so every RootNode-facing route there returns a clean
503 until ROOTNODE_API_URL/ROOTNODE_INTEGRATION_KEY point at a real deployment.

Updates PHASE10_LAUNCH_READINESS.md Gate 10 and its matrix row, and WINTER_BREAK_LAUNCH_CHECKLIST.md
and WINTER_BREAK_CAMP_OPERATIONS.md's own website-status notes, all of which previously said
'nobody has built the website-side form yet.'
2026-08-28 15:22:10 -05:00
ced4568 bfae2aafe0 docs(business): canonicalize RootNode V1 pricing matrix
docs/ROOTNODE_PRICING_AND_OFFERS_V1.md — the founder-approved, locked V1 pricing matrix (seasonal
camps, instructor-led programs, self-paced, mentoring/workshops, sibling policy, Winter Break
launch terms) recovered from synced conversation history after the original Phase 9.5 commit was
confirmed absent from canonical Git history. Cross-validated against the independently-prepared
RootNode Academy WHM+WHMCS Agent Handoff's own price guardrail table — identical figures.

Does not resolve or invent dates, venue, capacity, tax treatment, refund/cancellation policy, or
legal agreement language — those remain separate, open launch gates.

PHASE10_CANONICAL_STATE_AUDIT.md updated to reflect the recovery — the original Phase 9.5 commit
is still confirmed missing from history, but the pricing decision itself is now canonically
recorded rather than resting only on verbal confirmation. PC #2 inspection remains useful only to
check for an earlier local copy to compare against, not to locate the pricing itself anymore.
2026-08-28 14:06:03 -05:00
ced4568 74033b4cb8 docs(audit): reconcile Phase 10 findings on Phase 9.5, Infisical, and website pricing
Phase 9.5: reconfirmed no canonical commit exists (broader content search across full history —
still zero hits outside this session's own commits); Chase confirmed the real document lives on
PC #2, unpushed. Not duplicated here per Chase's explicit instruction — he owns bringing it to
canonical Gitea.

Infisical: Phase 10's 'project does not exist' finding was incomplete, not wrong about what it
checked — a proper org-scoped API enumeration (8 projects) still shows no RootNode Academy
project visible to the claude-code-migration machine identity, even though Chase confirms it
exists. Root cause narrowed to a missing per-project access grant (Infisical scopes identity
visibility per-project, separate from org membership) rather than the project not existing.
Corrected docs/SECRETS_AND_INFISICAL.md and docs/PHASE10_CANONICAL_STATE_AUDIT.md accordingly,
with the exact one-step fix (grant the identity access via Project Settings -> Access) and the
identity id needed to do it. No duplicate project was created.

Website pricing: a read-only fetch of rootnodeacademy.com found stale founding-price RANGES that
don't match the exact fixed V1 prices Chase gave directly this phase; Chase confirmed the fixed
prices in this repo's docs are correct and current. Noted as a real discrepancy for his own
awareness, not something this repo's scope covers fixing.

Updated the Phase 10 launch matrix with a Category column (TECHNICAL READY / OWNER DECISION
REQUIRED / EXTERNAL BLOCKER / HARDWARE BLOCKER) distinguishing who needs to act on each
remaining gate. No application code changed — audit/documentation reconciliation only.
2026-08-28 13:09:05 -05:00
ced4568 0b2303a3e2 docs(phase10): canonical state audit, secrets inventory, production/security/launch readiness
Gate 1 — canonical state audit: this workstation's HEAD/origin/main and every claimed milestone
SHA (Phase 2A through 9.1) verified consistent; no stranded branches/tags on Gitea; full-history
secret scan clean (no rotation required); Phase 9.5 confirmed absent from canonical history
(pricing was given directly by Chase and used without it); second-PC reconciliation commands
provided since this session cannot see that machine.

Gate 3 — secrets: real inventory derived from process.env greps across every service (not
guessed); Infisical auth fixed (wrong domain was the actual root cause of Phase 9.1's failures —
the identity belongs to infisical.cedshomelab.com, not app.infisical.com) but no RootNode
Academy project exists there yet, contradicting this phase's own authorization — flagged for
Chase to create.

Gates 4-9/15-16 — production: verified, not assumed, that no dedicated server/Proxmox VM has
been provisioned (empty terraform.tfvars, no state file, placeholder group_vars/production.yml)
— IaC validated (tofu validate, ansible --syntax-check both clean) and left unexecuted per this
phase's own 'do not fake production deployment' instruction. rootnodeacademy.com confirmed live
(Cloudflare, real HTTPS) — the separate, not-RootNode-owned website; no Platform API subdomain
exists yet.

Gate 18 — security audit: full review against every regression suite rerun this phase; no
launch-critical defect found.

Gates 19/20 — full regression (131 checks: e2e-phase5b/5c/6f/7/8/9, content validation,
migrations, frontend build) and the Winter Break commerce rehearsal, both rerun clean against
current main.

Gates 10-14/21 — website integration, Winter Break pricing (99/49, real, Stripe-validated),
agreements/legal, tax, and refund policy all reviewed; legal text/tax/refund policy/dates/venue/
capacity correctly left as open owner decisions, not invented.

No application code changed this phase — audit/launch-prep only.
2026-08-28 12:07:25 -05:00
ced4568 a4c0058f8c docs(stripe): document real Stripe TEST-mode validation results
PHASE9_1_STRIPE_TEST_VALIDATION.md rewritten from BLOCKED to the real, proven results: a real
Stripe TEST Checkout, a real TEST payment at the founder-approved $399 Founding Cohort price, a
genuine signed webhook, the existing registration->enrollment conversion, a real
duplicate-delivery proof, a real automated abandoned-checkout proof, a real refund with the
non-cascade rule reconfirmed, full Stripe<->RootNode reconciliation IDs, and the one real defect
found and fixed. STRIPE_INTEGRATION_RUNBOOK.md updated (real-Stripe local testing is no longer
'not yet exercised'; notes the no-sudo Stripe CLI install path and --api-key/--print-secret
usage). WINTER_BREAK_LAUNCH_CHECKLIST.md: Stripe TEST validation checked, price checked ($399
launch / $449 retail per Chase directly), live-mode items correctly still unchecked.
PHASE9_COMMERCE_AND_LAUNCH.md's Phase 9.1 addendum updated to reflect completion.
2026-08-28 09:47:11 -05:00
ced4568 fbf52d8334 test(stripe): add real-Stripe regression coverage and complete the opt-in TEST-mode check
e2e-phase9.js: one new deterministic regression test proving the webhook-correlation fix — a
test event referencing a transaction's own UUID (the exact shape a real Stripe
payment_intent.payment_failed/charge.refunded event now produces) is still matched. 20/20.

stripe-integration-test.js: configurable price/currency (STRIPE_TEST_PRICE_CENTS/
STRIPE_TEST_CURRENCY) and a STRIPE_TEST_MINOR=true guardian+minor path. Once a real payment
lands, now fully automates deep post-payment verification: exactly-one payment_transactions
record, registration/enrollment/cohort/guardian reconciliation, a genuine duplicate-delivery
proof (re-signing the REAL event fetched via stripe.events.retrieve — never a fabricated one),
real student activation + program-visibility, and a real refund with the non-cascade rule
reconfirmed — plus a fully automated, browser-free abandoned-checkout proof via Stripe's own
sessions.expire() API. Still opt-in, still a guaranteed no-op by default, still refuses
live-mode keys.
2026-08-28 09:47:01 -05:00
ced4568 c86b3de611 fix(payment): correlate PaymentIntent/Charge-referencing webhooks to the right transaction
Real Stripe TEST traffic (Phase 9.1) exposed a genuine defect the deterministic TestProvider
never could: payment_intent.payment_failed and charge.refunded events reference Stripe's
PaymentIntent/Charge object, never the Checkout Session, but payment_transactions.
provider_reference only ever stored the Session id. A real failure or refund webhook would have
silently failed to match any transaction (UNKNOWN_TRANSACTION).

- payment-provider.js: session creation now also sets payment_intent_data.metadata (the same
  metadata already carrying our transaction_id), so it propagates onto the PaymentIntent Stripe
  creates. normalizeEvent() (now async) reads transaction_id back from the PaymentIntent's own
  metadata directly for payment_intent.payment_failed, and via one extra paymentIntents.retrieve
  call for charge.refunded (Charges don't inherit PaymentIntent metadata automatically).
- commerce.js: the transaction lookup now matches provider_reference OR transaction_id — one
  additional OR clause, no schema/migration change.
- webhook-routes.js: added the now-required await on verifyAndParseWebhook (TestProvider's
  synchronous return is unaffected).
- docker-compose.dev.yml: wires PAYMENT_PROVIDER/STRIPE_SECRET_KEY/STRIPE_WEBHOOK_SECRET through
  to the api container (previously entirely absent — Phase 9 never needed it against the
  deterministic provider), empty/unset by default matching EMAIL_PROVIDER's own convention.

Verified fixed against a real Stripe TEST refund (Phase 9.1); e2e-phase9.js 20/20 and the Winter
Break commerce rehearsal 13/13 rerun clean after the fix.
2026-08-28 09:46:51 -05:00
ced4568 77fd1869db docs(stripe): document Phase 9.1 blocked status and live-mode readiness checklist
New: PHASE9_1_STRIPE_TEST_VALIDATION.md — baseline verification, the exact Stripe-credential
search performed (none found), the Stripe architecture review (dynamic price_data, unchanged),
what was proven vs. blocked per the mission's own step list, and the live-mode readiness
checklist. Updated: STRIPE_INTEGRATION_RUNBOOK.md and WINTER_BREAK_LAUNCH_CHECKLIST.md to point
at the new opt-in test and the blocked status. PHASE9_COMMERCE_AND_LAUNCH.md gets a short
addendum. No application code changed this phase — no genuine defect was exposed without real
Stripe traffic, so no regression rerun was required.
2026-08-27 10:40:53 -05:00
ced4568 f0ff14af38 test(stripe): add opt-in real Stripe TEST-mode integration check
RUN_STRIPE_INTEGRATION_TEST=true (default: no-op, zero Stripe network traffic). Refuses to run
against a live-mode secret key. Automates what's provable via pure API alone — real Checkout
Session creation verified against Stripe's own retrieve API, server-authoritative pricing proven
against Stripe's own recorded amount even under a tampered caller amount, invalid-signature
webhook rejection with no domain side effect — then polls RootNode's status endpoint for a
human/Stripe-CLI-completed real payment rather than fabricating a signed success event. Never
runs as part of normal regression/CI.
2026-08-27 10:40:42 -05:00
ced4568 80a5f6e457 docs(phase9): document commerce integration and Winter Break launch readiness
New: PHASE9_COMMERCE_AND_LAUNCH.md, WEBSITE_REGISTRATION_AND_CHECKOUT.md,
STRIPE_INTEGRATION_RUNBOOK.md, WINTER_BREAK_LAUNCH_CHECKLIST.md. Updated:
ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (commerce boundary + PHASE 9 BUILT rows),
OFFERING_AND_REGISTRATION_MODEL.md (pricing, the payment/commerce boundary, three-status
principle), WEBSITE_ENROLLMENT_INTEGRATION.md (price fields, real agreement/checkout/webhook
sections replacing Phase 8 placeholders), WINTER_BREAK_CAMP_OPERATIONS.md (real payment step
replacing the manual placeholder).
2026-08-27 09:06:04 -05:00
ced4568 281d60bc2d test(camp): add Winter Break commerce rehearsal
13/13 against a real, freshly-built stack, PAYMENT_PROVIDER=test (no live money): admin
configuration including price/payment-provider readiness, minor + adult registration through
real checkout and a real webhook call, duplicate-webhook idempotency, failed-payment seat
retention, abandoned-checkout seat release, a last-seat checkout race, a real classroom session,
and a real Lab task completed by the newly-activated minor student.
2026-08-27 09:05:54 -05:00
ced4568 8b3e56e402 test(phase9): add commerce integration acceptance suite
19/19: pricing in the public serializer, checkout gated on agreements, server-authoritative
amount, webhook signature verification and idempotency, payment success/failure/abandonment,
last-seat checkout race, reservation reaper, manual refund with no cascade, minor guardian
routing, admin/RBAC payment-data restrictions, audit/metric hygiene, the status-lookup endpoint,
and confirmation that ROOT_ V1/free-offering/classroom flows are unaffected.
2026-08-27 09:05:46 -05:00
ced4568 f0dde9b84a feat(admin): add offering pricing and payment/refund UI
- admin-offerings-routes.js: PATCH /api/admin/offerings/:id (allowlisted fields), GET
  /api/admin/registrations/:id/payments, POST /api/admin/payments/:id/refund
- frontend: Pricing card on offering detail, Payments card (transaction history + record-refund
  action) on registration detail — admin/owner-gated, reusing existing admin UI conventions
2026-08-27 09:05:36 -05:00
ced4568 16622bd9ab feat(webhook): add verified payment webhook route and website checkout/agreement contract
- webhook-routes.js: POST /api/webhooks/payment — signature-verified, idempotent, always 200
  once verified (a forged/invalid signature is a clean quiet 400)
- integration-routes.js: POST /api/integrations/registrations/:id/agreements (closes Phase 8's
  gap — agreement acceptance now on the public registration contract), POST .../:id/checkout
  (server-to-server, amount always server-side), GET .../:id/status (minimal safe status lookup
  for the website's success page — no PII, no amount, no provider reference)
- public-offerings-routes.js: price_amount/price_currency/requires_payment in the public serializer
- metrics.js: checkout/payment/webhook/conversion/seat-released counters, all low-cardinality
- notifications.js: listForRegistration now unions by the registration's own enrollment_id too,
  so the post-conversion activation notification shows up in the registration's communication
  history (previously only pre-conversion notifications were visible)
2026-08-27 09:05:29 -05:00
ced4568 b0e07bb17f feat(commerce): add payment-provider abstraction, commerce layer, and pricing schema
- migration 037: offerings.price_amount/price_currency, offering_registrations.reservation_expires_at,
  payment_transactions, payment_webhook_events (webhook_event_id as real PK — the idempotency mechanism)
- payment-provider.js: StripeProvider (real SDK, HMAC webhook verification) + TestProvider
  (deterministic, in-process, no network) behind one interface, selected via PAYMENT_PROVIDER
- commerce.js: createCheckoutSession (server-side price, never caller-supplied), processWebhookEvent
  (idempotent), handlePaymentSuccess/Failure/CheckoutAbandoned/Refund, reservation reaper support
- offerings.js: computeReservationExpiry, updateOffering (allowlisted PATCH), readiness gained
  price/payment_provider checks, convertRegistrationToEnrollment now fires the activation email
  every other enrollment path already triggers, and links the registration's guardian_id into
  student_guardians on conversion (real bug: a converted minor had no guardian link, so the new
  activation email had nowhere to resolve a recipient to)
- server.js: raw-body middleware scoped to /api/webhooks/payment, reservation-reaper poller
2026-08-27 09:05:19 -05:00
ced4568andClaude Sonnet 5 788fb197e0 docs(phase8): document enrollment operations
New: PHASE8_ENROLLMENT_OPERATIONS.md (phase summary), WINTER_BREAK_CAMP_OPERATIONS.md
(the real operational walkthrough), REGISTRATION_AND_GUARDIAN_COMMUNICATIONS.md,
OFFERING_ADMIN_GUIDE.md.

Updated: ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (BUILT NOW/PHASE 8 BUILT
table), OFFERING_AND_REGISTRATION_MODEL.md (both ordering bugs, launch readiness,
registration enforcement), STUDENT_GUARDIAN_DATA_MODEL.md (guardian communications now
wired), WEBSITE_ENROLLMENT_INTEGRATION.md (the real, live registration contract with
worked examples, replacing the old speculative future-flow section), platform/README.md
(test list).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 23:59:42 -05:00
ced4568andClaude Sonnet 5 aa63d09fb8 test(phase8): add enrollment operations coverage and Winter Break registration rehearsal
- e2e-phase8.js (16/16): offering CRUD/transitions/readiness, registration open/closed
  enforcement, capacity + concurrent final-seat race through the real registration
  endpoints, the public serializer's safety, website integration idempotency (sequential +
  concurrent), adult/minor paths, required-agreement conversion blocking with version
  snapshotting, offering->cohort membership, guardian recipient resolution, RBAC, and
  confirmation ROOT_ V1/self-paced/classroom flows are unaffected.
- test-phase8-winter-camp-registration.js (12/12): the full commercial-path story with no
  payment code — website submits a real minor registration (real guardian created/matched),
  guardian agreement acceptance, a simulated admin-side payment confirmation, conversion to
  real enrollment/cohort, real account activation, an independent adult path, two students
  genuinely competing for the final seat, and a REAL Lab task completed by the minor
  student with the instructor then seeing the real progress.
- e2e-phase7.js: fixed two now-stale test assumptions the new server-authoritative
  registration-window enforcement broke (an offering left in draft that Phase 7's own test
  never transitioned before registering against it) and widened a file-allowlist check for
  the new, legitimate public-offerings-routes.js.

Regression: e2e-phase6f.js 17/17, e2e-phase5b.js 32/32, e2e-phase5c.js 29/29, all fresh-stack.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 23:59:32 -05:00
ced4568andClaude Sonnet 5 40362ec0c9 feat(admin): add Offerings operations UI
Offerings list + create; offering detail with overview (valid-transitions-only buttons),
server-computed launch readiness, required agreements, and registrations/roster;
registration detail with lifecycle transitions, agreement status, communication history
(masked recipients), and convert-to-enrollment. Reuses the existing Admin.module.css/
adminApi conventions exactly (ProgramsView.jsx's own list+detail pattern) — no new UI
framework or pattern introduced.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 23:59:20 -05:00
ced4568andClaude Sonnet 5 24eee80354 feat(offering): launch readiness, agreement requirements, guardian communications, website registration API
- Registration is now server-authoritative: draft/cancelled/completed/full/window-closed
  offerings reject registration attempts with distinct error codes (assertRegistrationOpen).
- offering_agreement_requirements + conversion-time blocking on missing required
  agreements; accepted_by_type gained 'registrant' for an adult self-accepting before
  their student_id exists.
- computeOfferingReadiness() — a server-derived launch checklist (curriculum, instructor,
  schedule, location, capacity, registration window, agreements, curriculum package).
- Guardian communications wired into the existing Phase 5C notification system: a guardian
  email fallback in resolveRecipient(), a parallel enqueueForRegistration()/
  resolveRegistrationRecipient() pair for pre-account registration-stage notifications
  (registration_received/confirmed/needs_action, agreement_required, camp_operational_update),
  five new RootNode-branded templates. Same table, same worker, same masked admin view —
  no second notification system.
- public-offerings-routes.js: the unauthenticated, explicitly-allowlisted offering read API.
- POST /api/integrations/registrations: the website's idempotent registration submission
  contract, same shared-secret boundary as the existing enrollment integration endpoint.

Two real bugs found and fixed during this phase's own testing before any commit:
(1) the five new notification_type values were wired into the worker before the DB's own
CHECK constraint was widened to accept them — every registration-stage notification was
silently failing until a comprehensive smoke test caught it (036 fixes the constraint;
every previously-silent notification catch now at least logs).
(2) firing a notification right after a registration's insert, before its seat reservation
could still fail and roll the row back, created a real foreign-key violation on rollback —
fixed by reordering createRegistration() to validate -> reserve -> insert -> notify.

See docs/OFFERING_AND_REGISTRATION_MODEL.md and docs/PHASE8_ENROLLMENT_OPERATIONS.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 23:59:11 -05:00
ced4568andClaude Sonnet 5 df5e39f767 docs(phase7): document the business/product architecture foundation
New: ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (canonical business/platform
bridge — course families, delivery modes, BUILT NOW/PHASE 7 BUILT/FUTURE), OFFERING_AND_
REGISTRATION_MODEL.md (lifecycle diagrams, capacity mechanics), STUDENT_GUARDIAN_DATA_
MODEL.md (data minimization, RBAC, legal-review items), SCHOOL_AND_COMMUNITY_DELIVERY_
ARCHITECTURE.md (organization/location mapping, explicitly not multi-tenancy),
PHASE7_PRODUCT_ARCHITECTURE.md (phase summary).

Updated: WEBSITE_ENROLLMENT_INTEGRATION.md (documented future offering/registration flow,
current live contract unchanged), platform/README.md (test list).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 22:44:13 -05:00
ced4568andClaude Sonnet 5 7d870d8e2b test(phase7): add business/product architecture acceptance suite and Winter Camp rehearsal
- e2e-phase7.js (17/17): organizations/locations, guardians, curriculum packages
  (reference-only), offering capacity/transitions, registration lifecycle, agreements,
  registration-to-enrollment conversion, RBAC, age-guidance-independence, and confirmation
  that existing ROOT_ V1 self-paced/classroom flows are unaffected.
- test-phase7-winter-camp.js (18/18): the full Winter Break Camp operational path with NO
  payment code invoked — organization/location/curriculum-package/cohort/offering setup,
  guardian + adult registration, agreement acceptance, a simulated admin-side commerce
  confirmation, conversion to real enrollments and real cohort membership, real account
  activation via real one-time tokens, a real classroom session, and a REAL Lab task
  (linux-inspect-hardware) completed by the minor student inside a real disposable Lab
  container, confirmed to survive a real logout/login.

Regression: e2e-phase6f.js 17/17, e2e-phase5b.js 32/32 (including its own embedded Phase 5A
checks) on independent fresh stacks.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 22:44:02 -05:00
ced4568andClaude Sonnet 5 3510979196 feat(platform): business/product architecture — organizations, guardians, offerings, registrations
- organizations, locations, guardians, student_guardians (033) — additive, no existing
  table's meaning changes. Widens programs.family to accept 'bootcamp'.
- curriculum_packages/curriculum_package_items, offerings, instructor_offerings,
  offering_registrations, agreement_definitions/agreement_acceptances (034) — the full
  purchasable-delivery model. Server-defined status transitions only (no client-controlled
  offering/registration status). Capacity uses a single atomic UPDATE (reserved_seats),
  never a SELECT-count-then-INSERT race.
- offerings.js — business logic: reserveSeat/releaseSeat, transitionOffering/
  transitionRegistration (fixed transition tables), convertRegistrationToEnrollment
  (reuses enrollment.js's existing resolveOrCreateStudent/createManualEnrollment rather
  than duplicating student-matching logic).
- enrollment.js — resolveOrCreateStudent gains an optional, default-off returnToken param
  (used only by offerings.js's admin-only conversion route; the public website integration
  endpoint's contract is completely unchanged, per docs/WEBSITE_ENROLLMENT_INTEGRATION.md).
- admin-offerings-routes.js — the full admin/owner API surface. Registration/guardian
  detail is admin/owner only; offering/location listing is any instructor (access_notes
  stripped for a plain instructor).
- A real bug found and fixed during this phase's own smoke testing before any commit: the
  registration route originally reserved a seat BEFORE validating the registration's own
  fields, letting a validation failure consume a real seat and get masked behind a false
  OFFERING_FULL under a full offering. Fixed: validate (create) first, reserve second, roll
  back the registration row if reservation then genuinely fails.

See docs/ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md and
docs/OFFERING_AND_REGISTRATION_MODEL.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 22:43:52 -05:00
ced4568andClaude Sonnet 5 d066410451 docs(phase6f): document ROOT_ V1 pilot readiness
Release to Docker Hub / Build & Push Docker Image (push) Canceled after 0s
Release to Docker Hub / Create GitHub Release (push) Canceled after 0s
New: PHASE6F_ROOT_V1_PILOT_READINESS.md (phase summary), ROOT_V1_CURRICULUM_MANIFEST.md
(canonical V1 scope), ROOT_V1_PREREQUISITE_MAP.md, ROOT_V1_ASSESSMENT_MAP.md,
ROOT_V1_SYLLABUS.md, ROOT_V1_INSTRUCTOR_GUIDE.md, ROOT_V1_STUDENT_REQUIREMENTS.md,
ROOT_V1_INSTRUCTOR_LED_PILOT.md, WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md.

Updated: ROOT_PROGRAM_ARCHITECTURE.md (course-sequence table reflects real V1 statuses),
ROOT_PILOT_CURRICULUM.md (self-paced-readiness correction), ROOT_PROGRAM_DURATION_MODEL.md
(real post-integration totals: 64.2 V1 hours / 68.3 program-wide), ROOT_CAPSTONE_READINESS.md
(§1b, the new root-09 course-layer gate), platform/README.md (test list).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:46 -05:00
ced4568andClaude Sonnet 5 bd55b74b4f test(pilot): add multi-student and end-to-end ROOT_ V1 rehearsals
- test-phase6f-root-v1-pilot.js (22/22): a real cohort + live classroom session with three
  students in genuinely different states — on track, a real Lab validation failure/help-
  request/fix cycle, and a real admin Lab-reap/recreate with academic-state persistence
  proven, not assumed. Instructor visibility distinguishes all three.
- test-phase6f-root-v1-end-to-end.js (14/14): one student, entirely through the real
  student API, ROOT 01 through Capstone-eligible, including a real nested-Docker
  container-networking fix and an explicit proof that the real rootnode-capstone assessment
  is genuinely startable at the end — not just that the course tree says complete. Found and
  documented (not fixed) a real, pre-existing gap: the legacy six-stage Capstone unlock_rule
  requires two scenario sets (root-07-m01/m02) that are not course-layer prerequisites of
  anything on the path to root-capstone.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:35 -05:00
ced4568andClaude Sonnet 5 2864fe03dc test(phase6f): add V1 pilot acceptance suite; fix regression from real curriculum changes
- New platform/test/e2e-phase6f.js (17 tests): V1 manifest/exclusion correctness, the full
  V1 prerequisite graph, ROOT 01-04 re-verification, the orientation module's unlock
  behavior, optional-activity handling, the strengthened Capstone gate in both directions,
  instructor-notes RBAC on locked (not just draft) content, isolation, and an explicit
  age-independence check.
- foundations-helper.js: completeThroughRoot06/completeThroughRoot10 updated for the new
  practice-bank intro-lesson activities; new completeThroughCapstoneEligible() helper
  (ROOT 07-m03/m04 + full ROOT 09 + ROOT 12) shared by e2e-phase6f.js and both new drills.
- e2e-phase6a/6b/6d/6e.js: resume-pointer assertions updated for the new orientation
  module; e2e-phase6a.js's and e2e-phase6b.js's own inline ROOT 06/07/09/10/12 completion
  sequences updated for the new intro-lesson activities and the new root-09 Capstone
  dependency; e2e-phase6b.js's stale ROOT 09 status assertion and its draft-activity test
  (previously targeting the now fully-excluded ROOT 08) corrected.
- test-phase6d/6e-foundations-rehearsal.js: same orientation-module fix applied to the
  real-Lab dress rehearsals.

All of 6A (23/23) / 6B (26/26) / 6C (17/17) / 6D (21/21) / 6E (23/23) / 6F (17/17) verified
green on independent fresh stacks. See docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:21 -05:00
ced4568andClaude Sonnet 5 f695b7c95d feat(content): author the ROOT_ V1 orientation module and three practice-bank intro lessons
- root-01-m00-welcome-to-root: program overview + Lab-lifecycle orientation (disposable
  Labs, permanent progress, hints, reset/recreate, help-requests).
- One short intro-lesson activity for each of the three bare practice-bank modules
  (root-06-m01, root-07-m01, root-10-m02), explaining what the bank is and why it has no
  per-scenario teaching of its own — the minimum real authoring needed for genuine
  self-paced readiness without reclassifying them optional (see the curriculum migrations).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:09 -05:00
ced4568andClaude Sonnet 5 e48710a0be feat(curriculum): integrate ROOT_ V1 scope — status corrections, ROOT 08 exclusion, Capstone/ROOT 09 gate, orientation
- Correct stale courses.status for ROOT 05/06/07/09/10/12/Capstone (all modules were
  already ready; the course-level field was never bumped in an earlier phase).
- Exclude ROOT 08 (Storage & Scheduling) from ROOT_ V1 via visibility=draft — nothing
  downstream requires it, and leaving it visible mid-sequence would look like a skipped course.
- Self-correct a same-phase mistake: reclassifying the three instructor-led-only practice
  banks (root-06-m01/root-07-m01/root-10-m02) as optional would have broken Capstone
  reachability, since their scenarios are the literal legacy stage-gate content. Kept them
  required and gave each a real intro-lesson activity instead.
- Strengthen root-capstone's prerequisites to require ROOT 09 (Security) alongside ROOT 12
  (Real Ops) — the Capstone's own scenario pool has zero security overlap, and ROOT 09 only
  reached genuine READY status this phase.
- Add a required orientation module (root-01-m00-welcome-to-root) explaining the program,
  the Learn/Practice/Lab/Troubleshoot/Validate loop, and Lab-lifecycle expectations — no
  orientation content existed anywhere before this phase.

See docs/ROOT_V1_CURRICULUM_MANIFEST.md and docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:00 -05:00
ced4568andClaude Sonnet 5 87ad1251ce docs(phase6e): document the ROOT Foundations Completion milestone
Adds PHASE6E_ROOT_FOUNDATIONS_COMPLETION.md (the phase's own record: what was authored, the
dash-vs-bash scenario bug found and fixed, the pre-existing lock-enforcement gap found and
fixed, the regression fallout that fix revealed in earlier suites and why each was a stale test
assumption rather than a platform bug, and honest test/rehearsal/regression results).

Updates ROOT_FOUNDATIONS_PILOT_READINESS.md (full re-audit: all 4 ROOT Foundations courses now
READY, not inherited blindly from Phase 6D), ROOT_PROGRAM_DURATION_MODEL.md (real ROOT 02-04
duration data: 24.7 combined hours, landing inside Phase 6D's own estimated range; 67.9 hours
across the full authored program), ROOT_PILOT_CURRICULUM.md (ROOT 02-04 move from NOT IN PILOT
to IN PILOT with full module-level detail; the pilot is now genuinely self-paced-capable for the
entire foundational half, not just instructor-led), ROOT_PROGRAM_ARCHITECTURE.md and
ROOT_CURRICULUM_COVERAGE_MATRIX.md (course/module/scenario totals), and
ROOT_FOUNDATIONS_ARCHITECTURE.md (marked as-built, not just as-designed).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:09:02 -05:00
ced4568andClaude Sonnet 5 59edceea1f test(phase6e): acceptance suite, real-Lab rehearsal, and regression fixes for the lock fix
Adds platform/test/e2e-phase6e.js (23 checks): full ROOT 02/03/04 hierarchy/content/assessment-
pool verification, the complete ROOT 01->02->03->04->05 chain proven on one student (each
course's completion unlocking exactly the next), the direct-API lock bypass proven blocked on
both a Phase 6E course pair and a pre-existing unrelated pair, existing ROOT 06+/Capstone/
is_required behavior confirmed unaffected, self-paced no-instructor-deferral scan,
instructor-notes exclusion, RBAC, persistence, isolation, version stability, content validation.
23/23 pass against both a warm and a freshly-reset stack.

Adds platform/test/foundations-helper.js — a shared completeRootFoundations01Through04/
completeThroughRoot06/completeThroughRoot10 helper, extracted once multiple suites needed the
identical real-completion sequence as test setup, rather than duplicating it four times.

Adds infra/scripts/drills/{run,test}-phase6e-foundations-rehearsal — the real Platform API/
Gateway/Provisioner/disposable-Lab/nested-Docker/Git/k3s dress rehearsal: one real Lab task per
new course (nginx inspection, a real merge conflict, a real container-networking fix) plus the
full chain to a genuine ROOT 05 unlock, with k3s reconfirmed healthy and untouched at the end.
13/13 pass against a real, freshly-built stack.

Fixes the regression fallout from the (correct) course-routes.js lock fix: several earlier
suites' own historical shortcuts had accumulated small ordering/scenario gaps the missing
enforcement had been silently covering for — none were platform bugs. e2e-phase6a/6b/6c/6d.js
now inject the real ROOT 01-04 (and where needed ROOT 05/06/10) completion sequence via
foundations-helper.js before touching ROOT 05+ content; e2e-phase6b.js's bypass-student flow now
completes deployment-rollback/job-task/cronjob-task/container-logging-mcq explicitly (real
scenarios added in later phases that its original ALL_STAGES list never included); e2e-phase6a/
6d.js's own ROOT 01/05 completion tests now complete each module fully (through its lab and
reflection) before moving to the next, since those courses' own modules carry real intra-course
prerequisites. Two now-obsolete Phase-6D-era assertions in e2e-phase6d.js (ROOT 02-04 stay
PLANNED / stay vacuously unlocked) are updated to assert the opposite, with a comment explaining
Phase 6E intentionally changed that fact. All four suites re-verified green fresh.

Also fixes a real, reproducible dash-vs-bash bug caught while testing the new ROOT 01
rehearsal's ordering fix: `&>file` misparses under this image's actual /bin/sh (dash), breaking
a later `$!`-based kill — the rehearsal script now writes multi-line completion loops with
explicit status assertions, matching the discipline the other suites already use.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:08:48 -05:00
ced4568andClaude Sonnet 5 a09b7aaa53 fix(curriculum): enforce course/module lock on manual-marker completion
POST /api/student/activities/:activityId/complete previously checked only that the activity
exists and the student holds an active PROGRAM enrollment — never whether the activity's own
course/module was actually unlocked for that student. A direct POST could mark any
manual_marker activity (lesson/demonstration/knowledge_check/guided_exercise/reflection)
complete regardless of the real prerequisite chain, the exact class of gap Phase 6B already
fixed once for the capstone's own start route, just never closed for this more general one.

Found while proving the real ROOT 01->02->03->04->05 chain via direct API (this phase's own
explicit requirement) — and confirmed to predate Phase 6E entirely by reproducing it against an
unrelated, pre-existing ROOT 05/06 pair before touching any Phase 6E content.

Fix: the route now resolves the activity's own course/module through the exact same
buildCourseTree() lock computation every other route already trusts, and returns 403 { error:
'locked' } if either is locked — reusing the existing mechanism, not a second, duplicated check.
Verified live: both a ROOT 02 bypass attempt and a ROOT 06 bypass attempt are now correctly
rejected; legitimate unlocked completions are unaffected.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:08:28 -05:00
ced4568andClaude Sonnet 5 682ef2de47 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]>
2026-08-26 17:08:14 -05:00
ced4568andClaude Sonnet 5 e68b6f445a feat(root03): author Git & Collaborative Development
Adds all 6 ROOT 03 modules (why version control exists, the Git mental model, everyday Git,
branching & merging, remote repositories, engineering Git workflow) — 31 activities. Uses only
real LOCAL Git repositories throughout — no Gitea account, no network access, and no
RootNode-canonical-repo credential is ever needed or exposed in a student Lab
(docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §4). Modules 1, 2, and 6 are conceptual bookends with no
lab, matching the flexible readiness standard already used in ROOT 01/02.

Adds 3 new hands-on Git scenarios plus 2 MCQs: git-everyday-basics (init/add/commit, validated
by inspecting real `git log`/`git show` state, not just command output strings — Step 6's
explicit requirement), git-branching-and-merge (a REAL merge conflict caused on purpose by two
branches editing the same line, resolved by the student, validated by confirming the conflict
markers are gone and a real two-parent merge commit exists), and git-remote-clone-push (a real
local bare repository standing in for a remote — clone/commit/push validated by inspecting the
REMOTE side directly, not just the student's own clone). All tested end-to-end via the real
scenario-validation API, including negative tests.

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:08:00 -05:00
ced4568andClaude Sonnet 5 e007bcf000 feat(root02): author Linux Administration & Networking
Adds all 6 ROOT 02 modules (Linux system administration, storage & filesystems, the
Networking Mental Model, Linux network inspection, DNS & HTTP, Linux troubleshooting) — 37
activities, each with a real lesson, demonstration/guided exercise, knowledge check, hands-on
Lab, and reflection. The Networking Mental Model module (M3) gets deliberately extra depth per
its "critical module" designation, with no lab by design — conceptual mastery via knowledge
check and guided exercise, matching root-05-m00's own precedent.

Adds 6 new hands-on Linux/networking scenarios plus 2 MCQs: linux-service-inspection (inspects
the platform's own real nginx without touching it — a genuinely safe, real service-inspection
target that avoids ever risking the Lab's own connectivity), linux-service-lifecycle (start/
verify/stop a fully controllable service), linux-storage-inspection, linux-network-inspection
(ip/ss/getent/curl, each framed as answering one specific diagnostic question),
linux-dns-http-diagnosis (a genuine DNS-resolves-but-wrong-port failure, diagnosed and fixed),
and linux-troubleshooting-framework (a real missing-config-file failure, resolved via the
RootNode Observe/Evidence/Hypothesis/Test/Repair/Validate/Prevent framework). All tested
end-to-end via the real scenario-validation API, including negative tests confirming they fail
when unsolved.

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

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:07:46 -05:00
216 changed files with 16196 additions and 189 deletions
@@ -0,0 +1,66 @@
# Welcome to ROOT_
ROOT_ is RootNode Academy's path from "I can use a computer" to "I can run and repair real
infrastructure." It's built for anyone roughly high-school age and up — including college
students, career changers, help-desk technicians, junior sysadmins, and working developers who
want to move into DevOps, cloud, or platform engineering. There's no strict age cutoff: what
matters is whether you're ready for the material, not how old you are.
## Where ROOT_ goes
```
ROOT 01 Computing & Linux Foundations
ROOT 02 Linux Administration & Networking
ROOT 03 Git & Collaborative Development
ROOT 04 Containers & Docker
ROOT 05 Kubernetes Foundations
ROOT 06 Kubernetes Application Workloads
ROOT 07 Kubernetes Networking & Configuration
ROOT 09 Kubernetes Security
ROOT 10 Kubernetes Troubleshooting & Observability
ROOT 12 RootNode Real Ops
CAPSTONE Operate and Recover a Production-Like Environment
```
Each course builds on real skill from the one before it — you won't be asked to use a tool or
concept that a previous course didn't already teach you. By the end, you'll have operated a real
Kubernetes cluster, diagnosed and fixed real incidents, and demonstrated it in a hands-on
capstone — not answered multiple-choice questions about it.
## How you'll actually learn
Every module follows the same rhythm:
**Learn → Practice → Lab → Troubleshoot → Validate**
- **Learn** — a short lesson introduces one real concept, plainly, with no unexplained jargon.
- **Practice** — a demonstration or guided exercise lets you see the concept applied before
you're on your own with it.
- **Lab** — you do the real thing yourself, in your own disposable Lab environment: a real
Linux shell, real Git, real Docker, and (from ROOT 05 onward) a real Kubernetes cluster.
- **Troubleshoot** — Labs are built to sometimes not go as planned on the first try, on purpose.
Figuring out why something didn't work is as much the skill as doing it right the first time.
- **Validate** — your Lab checks its own result. You'll know immediately whether you actually
solved it, not just whether you think you did.
## Self-paced or instructor-led
ROOT_ is built to work either way. If you're going through it on your own, every module gives you
everything you need without a teacher in the room. If you're in an instructor-led class or camp,
your instructor can see your progress, help when you're stuck, and add their own pacing and
discussion on top of the same real curriculum — nothing about the platform changes between the
two, only who else is watching your progress.
## Assessments and the Capstone
A few courses end in a short formal assessment — a handful of real, timed Lab tasks (not a
multiple-choice quiz) that check you can actually do what the course taught, not just recognize
the right answer. The program ends with the **Capstone**: no more new teaching, just a
production-like environment with real incidents to find and fix, the same way you would on the
job.
## Your progress is permanent — your Lab is not
One thing to know before you start: the Lab environment you work in is disposable and can be
recreated at any time. Your academic progress is not. The next module covers exactly what that
means and why it's nothing to worry about.
@@ -0,0 +1,55 @@
# Your Disposable Lab
Every hands-on Lab in ROOT_ gives you a real, isolated environment — a real Linux shell from
ROOT 01 onward, real Git and Docker from ROOT 03-04, and a real Kubernetes cluster from ROOT 05
onward. It's yours alone while you're working in it. Here's what you need to know before your
first one.
## Disposable means disposable
Your Lab environment can be recreated at any time — when you start a new Lab, when an
instructor resets a classroom session, or if the environment needs to be refreshed. When that
happens, **anything that only exists inside the Lab disappears**: files you created in the
terminal, a local Git repository you initialized, a Docker container or image you built, a
Kubernetes object you deployed. None of that is backed up, and none of it needs to be — it was
never the point.
## Your academic progress is not disposable
What IS permanent, stored on RootNode's servers the moment you earn it: which lessons you've
read, which Labs you've completed, your assessment results, and your position in the course
sequence. Recreating your Lab never erases any of that. You can lose a Lab entirely and pick up
exactly where you left off in the course.
## How you know you actually solved it
Every Lab task validates itself — when you think you're done, you run the check and get a real
pass/fail answer immediately, based on the real state of your environment, not on your own
guess. If it fails, that's useful information, not a penalty: it's telling you specifically what
isn't right yet.
## If you get stuck
Most Labs have hints available, revealed one at a time so you can try the next step yourself
before seeing more. Using a hint doesn't cost you anything or affect your result — it's there to
keep you moving, not to be avoided on principle. A formal, timed assessment may disable hints;
if so, the assessment will tell you plainly before you start.
If a Lab's environment itself seems broken rather than the task being hard (a command that
should work doesn't, the environment won't start), use **Reset Scenario & Environment** to tear
down and rebuild it cleanly rather than fighting a broken environment.
## If you're in an instructor-led session
Your instructor can see your progress in real time and gets notified the moment you use **Need
Help** — no need to wait to be noticed. Use it for anything: a Lab that seems broken, a concept
that didn't click, or just wanting a second pair of eyes before you second-guess a working
solution.
## Returning to the course
When you're done with a Lab — pass or still stuck — you can always return to the course view and
come back to it later. Nothing about leaving a Lab mid-task loses your place.
You're ready. Let's start with how a computer actually runs the software you're about to spend
this program controlling.
@@ -0,0 +1,31 @@
# Knowledge Check: Welcome to ROOT_
---
**1. If your Lab environment is recreated, what happens to a file you saved inside it, and
what happens to your completed Labs so far?**
*Answer:* The file disappears — nothing inside the Lab persists across a recreation. Your
completed Labs and course progress are unaffected — those are stored on RootNode's servers, not
in the Lab, and survive regardless of what happens to any Lab environment.
**2. A Lab task fails your validation check. What should that tell you?**
*Answer:* Specifically what isn't correct yet, based on the real current state of your
environment — it's diagnostic information to act on, not a penalty.
**3. Name one thing to try before assuming a Lab is "just broken."**
*Answer:* Check the available hints (revealed one at a time), and if the environment itself
genuinely seems broken rather than the task being hard, use Reset Scenario & Environment to
rebuild it cleanly.
---
## Instructor Notes
**Facilitation:** This is a good moment to actually demonstrate a Lab reset live if you have
time — students remember "I saw it happen and nothing bad occurred" far better than being told
it's safe.
**Approximate timing:** 3 minutes.
@@ -0,0 +1,64 @@
# Linux System Administration
## Five questions every administrator asks
Whenever you're handed an unfamiliar running system, you're really always answering the same
five questions: **What is running? Who started it? Where is its configuration? Where are its
logs? How do I verify its state?** This module builds the vocabulary and tools for all five.
## Packages: how software gets installed
A **package manager** (`apt` on Debian/Ubuntu, `dnf`/`yum` on Fedora/RHEL) installs, updates, and
removes software along with tracking what depends on what — the alternative (manually downloading
and compiling everything) is exactly the kind of unreproducible mess package managers exist to
prevent. You won't install packages by hand often in this Lab, but recognizing `apt install` /
`apt list --installed` matters for reading real systems.
## Services: long-running background programs
A **service** is a program meant to run continuously in the background, outliving any single
terminal session — a web server, a database, a log collector. Unlike a command you run and wait
for, a service starts, keeps running, and is expected to still be there when you come back.
## systemd and systemctl (the concept — not available in this Lab)
On most real Linux servers, **systemd** is the process manager (PID 1) responsible for starting,
stopping, and supervising services, and `systemctl` is the command you'd use to control it
(`systemctl status nginx`, `systemctl restart nginx`, `journalctl -u nginx` for its logs).
**This disposable Lab container does not run systemd** — its PID 1 is a boot script that starts
k3s directly, the same way a real container almost never runs a full init system. This is worth
knowing honestly rather than pretending otherwise: the CONCEPTS below (a running process, its
config file, its logs, verifying its state) are identical whether the supervisor is systemd or
something else — this module's Labs teach those concepts against a service you CAN directly
inspect and control in this environment.
## Configuration files and environment variables
Most services read their settings from either a config file (a specific path, usually under
`/etc/`) or **environment variables** (`env` lists every variable in your current shell;
`export NAME=value` sets one). Knowing WHERE a service's configuration lives is exactly the
"where is its configuration" question from this lesson's opening — you'll answer it for real in
this module's first Lab.
## Logs: where a service tells you what happened
A well-behaved service writes what it's doing to a log — usually a file under `/var/log/`.
Reading logs (with `cat`, `tail`, `tail -f` — from ROOT 01) is almost always the first real step
in figuring out why something isn't working, which Module 6 builds into a full framework.
## Instructor Notes
**Lesson goal:** Establish the five-question mental model (running/started-by/config/logs/
verify) as the organizing frame for the rest of this module, and be explicit and honest that
systemd isn't available in this specific Lab environment rather than silently working around it.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** students who've used systemd elsewhere may expect
`systemctl` to exist here — address this directly and early rather than letting them discover
the error on their own.
**Discussion question:** "If you SSH into an unfamiliar production server and need to figure out
what's running and whether it's healthy, what's the very first command you'd run?" (There's no
single right answer — the point is getting students to articulate their own process before this
module hands them formal tools for it.)
@@ -0,0 +1,56 @@
# Demonstration: Answering the Five Questions For Real
## OBSERVE
This Lab's own terminal is served by a real running service: nginx.
```
$ pgrep -x nginx
906
```
Something with PID 906 is running.
## HYPOTHESIS
Predict: where would nginx's configuration file be, and where would its logs be?
## INSPECT
```
$ cat /etc/nginx/nginx.conf | head -5
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
...
$ ls /var/log/nginx/
access.log error.log
```
## CHANGE
Confirm it's actually listening on the port you'd expect:
```
$ ss -lnt | grep :80
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
```
## VERIFY
All five questions answered with real evidence, not assumptions: it's running (PID 906),
listening on port 80, configured at `/etc/nginx/nginx.conf`, logging to `/var/log/nginx/`. This
exact sequence — process, port, config, logs — is what you'll repeat for almost any unfamiliar
service on any real system.
## Instructor Notes
**Demo goal:** Show the five-question checklist answered end-to-end against a real, already-
running service the students are themselves using right now — more convincing than an
abstract example.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** stopping here at "PID exists" without also confirming it's
actually listening on the expected port — a process existing and a service actually being
reachable are two different facts, both worth checking.
@@ -0,0 +1,39 @@
# Knowledge Check: Linux System Administration
---
**1. What are the five questions this module opened with?**
*Answer:* What is running? Who started it? Where is its configuration? Where are its logs? How
do I verify its state?
---
**2. Why doesn't this Lab environment have `systemctl`?**
*Answer:* This Lab's container doesn't run systemd as PID 1 — its boot script starts k3s
directly instead, the same way most containers don't run a full init system. The concepts
(inspecting a running service's process/config/logs/state) are the same regardless of which
supervisor manages it.
---
**3. A process exists (`pgrep` finds a PID) but the service seems unreachable. What's the next
thing to check?**
*Answer:* Whether it's actually listening on the expected port (`ss -lnt`) — a running process
and a reachable service are two separate facts; the process could have started but failed to
bind its port.
---
**4. Where does a well-behaved Linux service typically write what it's doing?**
*Answer:* A log file, usually under `/var/log/`.
## Instructor Notes
**Facilitation:** Question 2 is worth confirming explicitly if any student has prior systemd
experience — set the expectation before the Lab, not after a confusing error.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,32 @@
# Guided Exercise: Plan Your Inspection Before You Type Anything
This is a paper exercise — commit to a plan before running commands in the Lab.
## Your task
You're told "the payments service seems slow." Before touching a keyboard, write down, in
order, which of these five questions you'd check FIRST, SECOND, and so on, and the one command
you'd use for each:
- Is it running at all?
- Is it listening on its expected port?
- What does its configuration say?
- What do its recent logs say?
- Has anything changed recently?
## A worked shape to check yourself against
A reasonable order: (1) is it running — `pgrep`/`ps`; (2) is it listening — `ss -lnt`; (3) recent
logs — `tail -n 50 <logfile>` (recent activity is usually more informative than config for a
sudden "slow" complaint); (4) configuration — only if logs point at a config-driven cause; (5)
recent changes — depends on context (deployment history, `ls -la` timestamps on config files).
There's no single correct order, but "running, then listening, then logs" before jumping to
config is a defensible default — you're checking the cheapest, fastest-answering questions
first.
## Instructor Notes
**Facilitation:** There's genuine room for disagreement on ordering here — the value is in
students defending their reasoning, not converging on one "correct" sequence.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,27 @@
# Reflection: You Now Have a Repeatable Checklist
## Reflection questions
1. Before this module, if someone said "check whether the service is healthy," what would you
have actually done? How does that compare to the five-question checklist now?
2. You started and stopped a service you fully controlled this module. What's different about
doing the same thing to a service you DIDN'T start, on a machine you don't fully understand
yet?
## Real Ops takeaway
**The five-question checklist works on ANY unfamiliar Linux system, not just this Lab.** Running
process, listening port, config location, logs, and current state — this is genuinely how
experienced engineers approach a machine they've never touched before. You'll use this exact
checklist again in ROOT 04 when a container "doesn't seem to be working," and again in
Kubernetes when a Pod isn't behaving as expected — the tools change, the checklist doesn't.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 is worth real discussion time — the
honest answer is "you should be more careful and inspect more before acting," a good early
seed for the least-privilege/caution habits ROOT 09 formalizes much later.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,50 @@
# Storage and Filesystems
## Block devices and partitions, conceptually
A **block device** is the kernel's view of a storage device (a disk, an SSD, a cloud volume) —
`lsblk` lists them. A device can be split into **partitions**, separate sections each formatted
with their own filesystem, though a single-partition disk is common too. You won't partition
anything in this Lab, but recognizing `lsblk`'s output (`sda`, `sda1`, `nvme0n1`...) matters for
reading real systems.
## Filesystems and mount points
A **filesystem** is the format that organizes raw blocks into the files/directories you actually
interact with (ext4, xfs, and — inside this container — overlay, a filesystem UNIQUE to how
containers layer their storage). A filesystem becomes usable by being **mounted** at some
directory path — `/` itself is a mount point, and additional filesystems can be mounted at other
paths (a separate disk mounted at `/data`, for example).
## Persistent vs. temporary storage — a distinction that matters immediately
Not everything written to disk is meant to last. `/tmp` is conventionally temporary (ROOT 01);
a container's own writable layer is, by convention and by ROOT 04's design, meant to be
DISPOSABLE — data written inside a container that isn't explicitly persisted elsewhere disappears
when that container is destroyed. This distinction — is this data supposed to survive, or not —
is the single most important storage question you'll carry forward into ROOT 04's volumes and
eventually Kubernetes' PersistentVolumes.
## Measuring usage: `df` vs. `du`
These answer two DIFFERENT questions, a common point of confusion:
- **`df -h`** — how much space is used/free on an entire mounted FILESYSTEM.
- **`du -sh <path>`** — how much space one specific DIRECTORY's contents actually use.
A filesystem can show plenty of free space overall (`df`) while one specific directory has
unexpectedly ballooned (`du`) — you need both questions answered to diagnose "why is disk full."
## Instructor Notes
**Lesson goal:** Build the persistent-vs-temporary distinction explicitly, since it's the direct
conceptual root of ROOT 04's bind-mount/volume distinction and, later, Kubernetes storage.
**Approximate timing:** 12 minutes read; 8 minutes live.
**Common misconception to watch for:** conflating `df` and `du` — a student surprised that
`df` doesn't show them "which directory is huge" is a good moment to reinforce they answer
different questions.
**Demo idea:** `df -h /` then `du -sh /var/log/*` side by side, narrating the different
question each answers.
@@ -0,0 +1,53 @@
# Demonstration: Two Different Questions, Two Different Commands
## OBSERVE
```
$ df -h /
Filesystem Size Used Avail Use% Mounted on
overlay 937G 199G 691G 23% /
```
The root filesystem overall: 23% used, plenty free.
## HYPOTHESIS
Predict: does this tell you anything about which specific directory is using the most space?
## INSPECT
```
$ du -sh /var/log/* 2>/dev/null
1.2M /var/log/nginx
850K /var/log/k3s.log
```
No — `df` only ever answers "how full is this filesystem overall." Finding a SPECIFIC large
consumer requires `du`, pointed at the directories you suspect.
## CHANGE
Narrow further into a single suspicious directory:
```
$ du -sh /var/log/nginx/*
600K /var/log/nginx/access.log
600K /var/log/nginx/error.log
```
## VERIFY
You now know both the overall filesystem picture (`df`) AND exactly which files are contributing
to it (`du`, narrowed progressively) — two genuinely different tools for two genuinely different
questions, not interchangeable.
## Instructor Notes
**Demo goal:** Make the df/du distinction concrete by actually needing BOTH to answer a
realistic "what's using my disk" question.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** running `du -sh /` directly (extremely slow on a large
filesystem, since it recurses everything) instead of narrowing progressively into suspected
directories first.
@@ -0,0 +1,31 @@
# Knowledge Check: Storage and Filesystems
---
**1. `df -h /` shows only 23% used, but you suspect one directory is unusually large. Does
`df` alone answer that?**
*Answer:* No — `df` reports overall filesystem usage, not per-directory. You need `du -sh <path>`
pointed at the suspect directory (or directories) to answer that.
---
**2. What makes data in a container's writable layer different from data in `/home`?**
*Answer:* By convention and design, a container's own writable layer is meant to be disposable —
data there disappears when the container is destroyed, unless explicitly persisted elsewhere
(a topic ROOT 04 covers directly). `/home` data is expected to persist normally.
---
**3. What does "mounting" a filesystem actually mean?**
*Answer:* Attaching a filesystem so it becomes accessible at a specific directory path — `/`
itself is a mount point, and additional filesystems can be mounted at other paths.
## Instructor Notes
**Facilitation:** Question 2 previews ROOT 04's volume/bind-mount content directly — worth
naming that connection explicitly.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,29 @@
# Guided Exercise: Narrow Down a Disk Problem on Paper
This is a paper exercise — commit to a plan before running commands in the Lab.
## Your task
`df -h /` shows the root filesystem is 95% full. You have three suspect directories:
`/var/log`, `/srv/cache`, and `/home`. Write the exact sequence of `du` commands you'd run to
find the actual culprit, narrowing your search efficiently rather than checking everything with
maximum depth immediately.
## A worked shape to check yourself against
```
du -sh /var/log /srv/cache /home
```
— one command, comparing all three top-level suspects at once. Whichever is largest, THEN
narrow further into it specifically (e.g. `du -sh /srv/cache/*` if that one's the culprit) rather
than deep-diving into all three from the start — this mirrors Module 1's "cheapest, fastest-
answering question first" principle.
## Instructor Notes
**Facilitation:** Point out that running `du -sh` separately three times versus once with all
three paths produces the same information — the point isn't syntax, it's the "compare broadly,
then narrow" strategy.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,27 @@
# Reflection: "It's Probably the Logs" Is a Guess, Not a Diagnosis
## Reflection questions
1. Before measuring, which of the three directories in this module's Lab did you guess would
be largest? Were you right?
2. Why does "persistent vs. temporary" matter enough to be worth memorizing as a question you
ask about EVERY piece of data, not just during a storage module?
## Real Ops takeaway
**Guessing which directory is consuming disk space, without measuring, is exactly the kind of
assumption that wastes real incident-response time.** `du`, applied progressively (broad, then
narrow), replaces guessing with evidence in seconds. And the persistent-vs-temporary question
you just practiced is the exact same question you'll ask again in ROOT 04 about container data,
and again much later about Kubernetes storage — a question worth internalizing now, cheaply,
rather than learning the hard way after data you assumed would persist didn't.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 works well as a quick poll before
revealing the real answer — the mismatch between guesses and reality reinforces the lesson
better than stating it directly.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,104 @@
# Networking Mental Model
## Why this module gets extra attention
Nearly everything you'll do for the rest of ROOT_ — container networking, Kubernetes Services,
Ingress, NetworkPolicy, DNS, cloud infrastructure — is a variation on the SAME small set of
ideas this module introduces. Get this model solid now, and every later networking topic becomes
"the same thing again, one layer up." Skip it, and every later module will feel like memorizing
unrelated facts. The goal here is operational REASONING, not subnet arithmetic — you will not be
asked to calculate a subnet mask by hand.
## The chain, layer by layer
Think of reaching another machine as passing through a fixed sequence of layers, each of which
can independently succeed or fail:
```
Host
↓
Network interface (this machine's connection to a network)
↓
IP address (this machine's address on that network)
↓
Subnet (which other addresses are "local" to reach directly)
↓
Default gateway (where to send anything NOT local)
↓
Routing (the actual path traffic takes)
↓
DNS (translating a name into an IP address)
↓
Port (which specific service on that IP)
↓
TCP / UDP (how the connection itself behaves)
↓
Application protocol (HTTP, etc. — the actual conversation)
```
## Each layer, briefly
- **Network interface** — your machine's actual network connection (`eth0`, etc.). No interface,
no networking, full stop.
- **IP address** — this machine's address on a given network. IPv4 addresses look like
`172.17.0.2`. `127.0.0.1` (aka `localhost`) is special: it always means "this same machine,"
never anywhere else. `0.0.0.0` is different again — when a SERVICE binds to it, it means
"listen on every interface this machine has," not a real destination address.
- **Subnet** — a range of addresses considered "local" to each other, expressed as CIDR
(`172.17.0.0/16` means every address starting `172.17.`). You don't need to calculate subnet
math by hand — you need to recognize that two addresses in the same subnet can talk directly,
while addresses outside it need a gateway.
- **Default gateway** — the address your machine sends traffic to when the destination ISN'T in
its own subnet — effectively "ask this router to figure out the rest."
- **Routing** — the actual decision-making about which path traffic takes; `ip route` shows your
machine's routing table (Module 4 makes this hands-on).
- **DNS** — translates a human-readable name (`example.com`) into an IP address, since IP
addresses aren't what humans want to remember or type.
- **Port** — a single IP address can host MANY services simultaneously; the port number is what
distinguishes "which one." Port 80 conventionally means HTTP, 443 HTTPS, 22 SSH — conventions,
not hard rules.
- **TCP vs. UDP** — TCP is connection-oriented and guarantees delivery/order (most web traffic,
SSH); UDP is connectionless and faster but delivers no guarantees (DNS queries themselves,
video streaming). Client/server: one side (the server) listens on a fixed port waiting for
connections; the other (the client) initiates them.
## The reasoning skill: "where is the failure?"
Given this chain, a real diagnostic question decomposes into a sequence of yes/no checks, each
isolating ONE layer:
- Does the host have an IP address at all?
- Is the destination local (same subnet) or remote?
- Is there a valid route to it?
- Can the name resolve to an IP?
- Can the destination PORT actually be reached?
- Is a service actually listening there?
- Does the application itself respond correctly?
Module 4 makes each of these a real command. Module 5 walks the complete request path end to
end. Module 6 formalizes this into RootNode's troubleshooting framework, applied to networking
specifically.
## Instructor Notes
**Lesson goal:** This module gets outsized weight for a reason — it's the single highest-
leverage conceptual investment in ROOT 02. Do not rush it to "cover the topics"; depth here pays
off across ROOT 04, ROOT 05-10, and beyond.
**Approximate timing:** 25 minutes read; 15-20 minutes live with discussion — longer than a
typical lesson, deliberately.
**Common misconception to watch for:** treating `0.0.0.0` as a real destination address rather
than a "listen on every interface" bind convention — this genuinely confuses students later when
they see it in a container's own listening-port output.
**Discussion question:** Walk through a real failure narrative live: "You `curl` a URL and get
'Connection refused.' Which layers does that rule OUT, and which remain suspects?" (Answer:
rules out DNS entirely — you got FAR enough to attempt a TCP connection — and rules out "nothing
is listening at all" being the ENTIRE problem elsewhere; "refused" specifically means something
actively rejected the connection, most often nothing listening on that exact port.)
**Optional extension:** contrast "Connection refused" vs. "Connection timed out" vs. "Could not
resolve host" as three DIFFERENT failure signatures pointing at different layers — worth a full
discussion pass if time allows, since Module 5 depends on students already sensing this
distinction.
@@ -0,0 +1,56 @@
# Demonstration: Three Failures That Sound Similar But Aren't
## OBSERVE
All three of these LOOK like "it didn't work" — but each one rules out and rules in completely
different layers.
```
$ curl http://this-name-does-not-resolve.invalid/
curl: (6) Could not resolve host: this-name-does-not-resolve.invalid
```
## HYPOTHESIS
Which layer does THIS failure point at — DNS, routing, the port, or the application itself?
## INSPECT
"Could not resolve host" happens BEFORE any network connection is even attempted — it's purely a
DNS failure. No IP was ever found to connect to.
## CHANGE
Now a different failure, against a name that DOES resolve:
```
$ curl http://localhost:9999/
curl: (7) Failed to connect to localhost port 9999: Connection refused
```
DNS/name resolution already succeeded here (`localhost` always resolves). "Connection refused"
means a TCP packet reached the destination and was ACTIVELY rejected — almost always because
nothing is listening on that port.
## VERIFY
A third signature, distinct from both:
```
$ curl http://10.255.255.1:80/
curl: (28) Failed to connect to 10.255.255.1 port 80: Connection timed out
```
No response at all, not even a rejection — usually means the destination is unreachable
(no route, or blocked silently), not that something is running but declining you.
## Instructor Notes
**Demo goal:** Get students to viscerally distinguish these three error MESSAGES as three
different diagnostic starting points, not interchangeable "it's broken" signals.
**Approximate timing:** 10 minutes live.
**Common mistake to watch for:** treating every connection failure as "the network is down" —
each of these three messages should immediately narrow which layer to investigate next, per
Module 5's troubleshooting exercise.
@@ -0,0 +1,45 @@
# Knowledge Check: Networking Mental Model
---
**1. What's the actual difference between an IP address and a port?**
*Answer:* An IP address identifies a specific machine on a network; a port identifies WHICH
service on that machine, since one IP can host many services simultaneously.
---
**2. `curl` reports "Could not resolve host." Which layer failed?**
*Answer:* DNS — the name never got translated into an IP address, so no connection attempt was
even made.
---
**3. `curl` reports "Connection refused" instead. What does that specifically tell you?**
*Answer:* The name resolved fine and the destination was reachable, but nothing is listening on
the port you tried — the connection attempt was actively rejected, not silently ignored.
---
**4. What is a default gateway for?**
*Answer:* It's where your machine sends traffic whose destination ISN'T in its own local subnet
— effectively "ask this router to figure out how to get there."
---
**5. True or false: `0.0.0.0` is a real destination address you could `curl`.**
*Answer:* False (in the way most students initially assume) — when a SERVICE binds to
`0.0.0.0`, it means "listen on every interface," a bind convention, not a real host you'd
connect to as a destination.
## Instructor Notes
**Facilitation:** Questions 2-3 are worth spending real time on together — the point isn't
memorizing the exact error text, it's the underlying "which layer does this rule out" reasoning.
**Approximate timing:** 8 minutes (slightly longer than a typical knowledge check, given this
module's weight).
@@ -0,0 +1,33 @@
# Guided Exercise: Diagnose From the Symptom Alone
This is a paper exercise — reason through each case using only the mental model, before any
hands-on Lab work.
## Your task
For each symptom, name which layer(s) it rules OUT, and which remain genuine suspects:
1. `ping 8.8.8.8` succeeds, but `curl https://example.com` fails with "Could not resolve host."
2. `dig example.com` returns a correct-looking IP address, but `curl https://example.com` times
out with no response at all.
3. `curl http://internal-service:8080/` returns "Connection refused," but
`curl http://internal-service:9090/` (a different port, same host) succeeds.
## A worked shape to check yourself against
1. Rules out: basic connectivity/routing to the internet (ping succeeded). Remaining suspect:
DNS specifically — something is preventing name resolution even though raw IP connectivity
works.
2. Rules out: DNS (resolution clearly succeeded). Remaining suspects: routing/reachability to
that specific IP, or a firewall silently dropping the connection — "no response at all" is the
timeout signature, not the "actively rejected" signature.
3. Rules out: DNS AND general reachability to that host (port 9090 worked). Remaining suspect:
narrowed all the way down to "nothing is listening on port 8080 specifically" — the most
precisely isolated diagnosis of the three.
## Instructor Notes
**Facilitation:** Item 3 is the strongest example of "isolate one variable" reasoning (same
host, different port) — worth highlighting explicitly as the gold-standard diagnostic pattern.
**Approximate timing:** 10-12 minutes live discussion; 8 minutes self-paced.
@@ -0,0 +1,30 @@
# Reflection: One Model, Reused Everywhere From Here On
## Reflection questions
1. Before this module, how would you have described "the network is broken" to a colleague?
How would you describe it now, more precisely?
2. This module deliberately avoided subnet math. Do you feel that cost you anything real, given
the module's actual goal (reasoning about WHERE a failure is, not calculating addresses)?
## Real Ops takeaway
**Every networking topic for the rest of ROOT_ is this same chain, viewed from a different
altitude.** A container's published port is still "port," a Kubernetes Service is still "how do
I reach the right destination via a stable address," Ingress is still "routing decisions," and
NetworkPolicy is still "which subnets/ports are allowed to reach which." You are not about to
learn a new mental model in ROOT 04 or ROOT 05 — you're about to see this SAME one applied one
layer up, repeatedly. That's why this module got extra weight: the investment compounds.
This module is complete. Continue to Module 4, where these ideas become real commands.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 is worth collecting several answers
before and after — the change in precision is itself the evidence this module worked.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced writing.
**Milestone note:** this is the "critical module" this course was built around — consider
confirming as a group that everyone can walk the full chain (host→interface→IP→subnet→gateway→
routing→DNS→port→TCP/UDP→application) from memory before moving on.
@@ -0,0 +1,74 @@
# Linux Network Inspection
## Every command below is really a question
Module 3 gave you the model. This module gives you the commands — but treat each one as
ANSWERING a specific question, not as syntax to memorize.
## "What addresses does this machine have?" — `ip addr`
```
$ ip -4 addr show eth0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
```
This machine's IPv4 address is `172.17.0.2`, on a `/16` subnet.
## "Where will this machine send traffic?" — `ip route`
```
$ ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.2
```
The second line says: anything in `172.17.0.0/16` is reachable directly. The first line (the
`default` route) says: everything else goes through `172.17.0.1` — the default gateway from
Module 3, now a real line in a real table.
## "What TCP ports are listening?" — `ss -lnt`
```
$ ss -lnt
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
```
Something is listening on port 80, on every interface (`0.0.0.0` as a BIND address, exactly the
convention Module 3 flagged). `ss` replaced the older `netstat` on modern Linux — same idea.
## "What does DNS say this name resolves to?" — `getent hosts` / `dig`
```
$ getent hosts example.test
10.0.0.5 example.test
```
`getent hosts` checks the same resolution path the rest of the system uses (local `/etc/hosts`
entries first, then DNS) — a reliable first choice. `dig <name>` queries DNS directly and shows
more detail, useful once you specifically suspect a real DNS server issue rather than a local
override.
## "Can the application actually respond?" — `curl`
```
$ curl -sf http://localhost:80/ -o /dev/null -w '%{http_code}\n'
200
```
Everything before this point (address, route, port, DNS) can be perfect, and the APPLICATION
itself can still be broken — `curl` is the only one of these tools that actually tests the full
chain end to end, including the application's own response.
## Instructor Notes
**Lesson goal:** Convert Module 3's mental model into muscle-memory commands, each explicitly
framed as answering one specific diagnostic question.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating all five commands as redundant ways to "check
the network" — reinforce that each answers a DIFFERENT layer's question, and a real diagnosis
usually needs more than one.
**Demo idea:** run all five in sequence against this Lab's own nginx, narrating which Module 3
layer each one answers.
@@ -0,0 +1,56 @@
# Demonstration: The Same Five Questions, Answered Live
## OBSERVE
```
$ ip -4 addr show eth0
inet 172.17.0.2/16 ...
```
"What address do I have?" — answered.
## HYPOTHESIS
Predict what `ip route` will show before running it — specifically, predict whether there will
be a `default` line.
## INSPECT
```
$ ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 ...
```
Confirmed — a `default` route exists, meaning traffic to anywhere outside `172.17.0.0/16` has
somewhere to go.
## CHANGE
```
$ ss -lnt
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
```
"What's listening?" — port 80, confirmed.
## VERIFY
```
$ curl -sf http://localhost/ -o /dev/null -w '%{http_code}\n'
200
```
The full chain, verified end to end with real evidence at each layer — not "the network seems
fine," but five specific, falsifiable answers.
## Instructor Notes
**Demo goal:** Chain all five commands together as ONE continuous diagnostic narrative, not
five disconnected syntax examples.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** running `curl` first and stopping at "it worked" without ever
having separately confirmed address/route/port — fine when things work, but the WRONG habit for
when they don't (Module 5's Lab requires the full sequence).
@@ -0,0 +1,39 @@
# Knowledge Check: Linux Network Inspection
---
**1. Which command answers "what TCP ports are listening on this machine"?**
*Answer:* `ss -lnt`.
---
**2. Which command answers "where will this machine send traffic that isn't local"?**
*Answer:* `ip route` — specifically, the `default` line in its output.
---
**3. Why is `curl` the only tool in this module that can confirm the APPLICATION itself is
working, not just the network path to it?**
*Answer:* Address, route, port, and DNS can all be perfectly correct while the application
behind them still returns an error or wrong content — `curl` is the only one of these tools that
actually completes an HTTP request and inspects the real response.
---
**4. `getent hosts` and `dig` both resolve names. When would you reach for `dig` specifically
instead of `getent hosts`?**
*Answer:* When you specifically suspect a DNS server issue and want to query DNS directly with
more detail — `getent hosts` checks the same path the rest of the system uses (including local
`/etc/hosts` overrides first), which is usually the right first choice, but doesn't show you as
much about what the actual DNS server itself said.
## Instructor Notes
**Facilitation:** Question 3 reinforces Module 3's "layers can each independently succeed or
fail" idea with a very concrete example.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,28 @@
# Guided Exercise: Which Command Answers This Question?
This is a paper exercise — commit to an answer before checking it in the Lab.
## Your task
For each question, name the ONE command you'd reach for first:
1. "Does this machine have a working IP address at all?"
2. "Is anything listening on port 8080 right now?"
3. "Does `payments.internal` resolve to an address?"
4. "Is the web server actually returning a valid response, not just accepting connections?"
5. "Where does traffic to an address outside my local subnet actually go?"
## A worked shape to check yourself against
1. `ip -4 addr show`
2. `ss -lnt | grep 8080`
3. `getent hosts payments.internal`
4. `curl -sf http://.../ -o /dev/null -w '%{http_code}'`
5. `ip route`
## Instructor Notes
**Facilitation:** Have students justify WHY their chosen command answers the specific question
asked, not just that it's "a networking command" — the goal is precision, not familiarity.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,25 @@
# Reflection: Five Commands, Five Answers, One Diagnosis
## Reflection questions
1. Which of these five commands did you already know before this module? Which was
genuinely new?
2. You now have a real toolkit for Module 3's "where is the failure" question. What's still
missing before you'd feel fully confident diagnosing a real production networking issue?
## Real Ops takeaway
**Five specific commands, each answering one specific question, beat one vague instinct that
"something's wrong with the network."** This is the exact toolkit real engineers reach for
first, in this order, when handed an unfamiliar connectivity problem. Module 5 puts all five
into one continuous diagnostic story; Module 6 formalizes the discipline around using them.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2's honest answer is usually
"experience with real, varied failures" — a good moment to set expectations that fluency
compounds with practice, not from one module.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,58 @@
# DNS and HTTP
## The complete request path
Every time a browser or `curl` fetches a URL, it walks the entire Module 3 chain in order:
```
Application (curl / browser)
↓
DNS lookup — translate the name to an IP
↓
IP address obtained
↓
Routing — figure out the path to that IP
↓
TCP connection established — a three-way handshake, underneath curl
↓
HTTP request sent
↓
Server processes it
↓
HTTP response returned
```
Nothing here is new — this is Module 3's chain and Module 4's commands, just walked start to
finish as ONE continuous story instead of five separate checks.
## Four distinct failure categories, not one
Given this path, "it doesn't work" always decomposes into exactly one of four categories:
1. **DNS failure** — the name never resolved to an IP at all (`curl`'s "Could not resolve
host").
2. **Routing/connection failure** — the IP was found, but no TCP connection could be established
— either refused (something's there but nothing's listening) or timed out (no response at
all, usually unreachable or silently blocked).
3. **TCP succeeded, but the application never responds correctly** — a connection was
established, but the server hangs, drops the connection, or the request never completes.
4. **HTTP/application failure** — a full HTTP response comes back, but it's WRONG (a 500 error,
a 404, unexpected content) — everything below the application layer worked fine.
These are genuinely different problems requiring genuinely different fixes — conflating them
(treating a 404 the same as a DNS failure) wastes real diagnostic time.
## Instructor Notes
**Lesson goal:** Make the four-category failure taxonomy explicit and distinct — this is the
direct foundation for this module's diagnostic Lab and for Module 6's formal framework.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating an HTTP 500 error the same category as "the
network is down" — a 500 means the ENTIRE network path worked; the failure is purely in the
application's own logic.
**Discussion question:** "A request returns HTTP 404. Which of the four categories is that, and
what does it rule out?" (Answer: category 4, application/HTTP — it rules out DNS, routing, AND
TCP connectivity entirely, since a real HTTP response came back at all.)
@@ -0,0 +1,57 @@
# Demonstration: Walking the Complete Path, Layer by Layer
## OBSERVE
```
$ getent hosts lab-internal.test
10.99.99.42 lab-internal.test
```
DNS: succeeded.
## HYPOTHESIS
Predict: if the next step (TCP connection) fails, what would that specific error look like?
## INSPECT
```
$ curl http://lab-internal.test:9091/
curl: (7) Failed to connect to lab-internal.test port 9091: Connection refused
```
"Connection refused" — DNS already succeeded (ruled out), the failure is specifically that
nothing is listening on port 9091 at that address.
## CHANGE
Find what IS actually listening, on the correct address:
```
$ ss -lnt | grep LISTEN
LISTEN 0 5 0.0.0.0:9095 0.0.0.0:*
```
The real service is on 9095, not 9091 — a configuration/expectation mismatch, not a DNS or
routing problem.
## VERIFY
```
$ curl http://lab-internal.test:9095/
app running
```
The full path, once pointed at the CORRECT layer that was actually broken (the port, not DNS),
succeeds end to end.
## Instructor Notes
**Demo goal:** Show that a real, plausible-sounding complaint ("DNS must be broken") is
disproven by evidence in seconds, and the REAL cause is one layer downstream.
**Approximate timing:** 10 minutes live.
**Common mistake to watch for:** jumping straight to "restart everything" instead of walking
the chain layer by layer to find the SPECIFIC broken step — the whole point of this module's
Lab is resisting that shortcut.
@@ -0,0 +1,40 @@
# Knowledge Check: DNS and HTTP
---
**1. Name the four failure categories this module introduced.**
*Answer:* DNS failure, routing/connection failure, TCP-succeeds-but-application-never-responds,
and HTTP/application failure (a full response comes back but it's wrong).
---
**2. A request returns HTTP 500. What does that tell you about DNS and TCP connectivity?**
*Answer:* Both worked completely — a 500 means a full HTTP response was received, so the failure
is purely inside the application's own logic, not the network path to reach it.
---
**3. Why is "connection refused" a MORE specific diagnosis than "timed out"?**
*Answer:* "Refused" means a packet reached the destination and was actively rejected (almost
always: nothing listening on that port) — you know the destination is reachable. "Timed out"
means no response at all, which could mean unreachable, no route, or silently blocked —
genuinely less specific.
---
**4. A teammate says "DNS must be broken" because a request fails. What should you check
BEFORE agreeing?**
*Answer:* Whether the name actually resolves at all (`getent hosts` or `dig`) — many failures
that "feel like DNS" are actually one layer downstream (routing, port, or application), and
DNS itself may be working perfectly.
## Instructor Notes
**Facilitation:** Question 4 mirrors this module's own Lab almost exactly — a good moment to
preview it if not already started.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,32 @@
# Guided Exercise: Categorize the Failure Before You Fix It
This is a paper exercise — commit to a category before checking it in the Lab.
## Your task
For each symptom, name which of the four failure categories (DNS / routing-connection /
TCP-hangs / HTTP-application) it belongs to:
1. `curl: (6) Could not resolve host: api.internal`
2. `curl: (7) Failed to connect to api.internal port 443: Connection refused`
3. A request eventually returns, but with HTTP 502 Bad Gateway
4. A request just hangs for 30 seconds with no response, then finally times out
## A worked shape to check yourself against
1. DNS — the name never resolved.
2. Routing/connection — DNS worked, but nothing accepted the connection on that port.
3. HTTP/application — a full HTTP response came back (502 is a real, complete response), so
DNS/routing/TCP all worked; the failure is in the application layer (often a proxy unable to
reach ITS OWN backend, which is itself worth investigating further).
4. TCP-hangs / ambiguous-timeout — this is the least specific signature; it could mean
unreachable, silently dropped by a firewall, or an application that accepted the connection
but never responds — worth narrowing further with `ss`/`ip route` before concluding.
## Instructor Notes
**Facilitation:** Item 3 (502) is a good one to slow down on — many students initially assume
any 5xx code means "the network is broken," when it actually proves the OPPOSITE for every
layer below the application.
**Approximate timing:** 10-12 minutes live discussion; 8 minutes self-paced.
@@ -0,0 +1,26 @@
# Reflection: You Fixed the Real Problem, Not the Suspected One
## Reflection questions
1. Before running any commands in this module's Lab, what would you have guessed was broken?
Was your guess right?
2. Why does correctly categorizing a failure (DNS vs. routing vs. TCP vs. application) matter
more than just "trying things until something works"?
## Real Ops takeaway
**"DNS must be broken" is one of the most common wrong first guesses in real incident response
— and disproving or confirming it takes seconds, not minutes.** The habit this module builds —
categorize the failure precisely before attempting a fix — is what separates a five-minute
diagnosis from an hour of trying unrelated things. This exact reasoning, applied to Kubernetes
Services/Ingress/DNS later, is one of the most valuable transferable skills in this entire
program.
This module is complete. Continue to the final module of this course.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 works well as a live poll before
revealing the Lab's actual root cause — the gap between instinct and evidence is the lesson.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,63 @@
# Linux Troubleshooting
## The RootNode troubleshooting framework
Every module in ROOT 02 has been building toward this: a repeatable, disciplined process for
diagnosing ANY Linux-level failure, not just the specific ones you've already seen.
```
OBSERVE
↓
GATHER EVIDENCE
↓
FORM HYPOTHESIS
↓
TEST
↓
REPAIR
↓
VALIDATE
↓
PREVENT
```
## Each step, and why skipping it costs you later
- **Observe** — what's the actual, specific symptom? Not "it's broken" — the exact error, the
exact command that failed.
- **Gather evidence** — logs, process state, listening ports, config content — the raw facts,
collected BEFORE guessing at a cause.
- **Form hypothesis** — based on the evidence (not a guess made before looking), what's the most
likely cause?
- **Test** — check whether your hypothesis is actually true, ideally without yet making changes
that could mask the real problem.
- **Repair** — make the minimal change that addresses the CONFIRMED cause, not a broad "try
everything" fix.
- **Validate** — confirm the original symptom is actually gone, with real evidence, not
assumption.
- **Prevent recurrence** — what would stop this from happening again? Skipping this step is how
teams end up fixing the exact same incident repeatedly.
## Why "skip straight to fixing" costs you
Jumping straight from OBSERVE to REPAIR (skipping evidence and hypothesis) means you're guessing
— sometimes you'll get lucky, but you won't have LEARNED anything transferable, and you risk
"fixing" a symptom while leaving the actual cause untouched, guaranteeing a repeat. Every command
from Modules 1, 2, and 4 IS the "gather evidence" step, made concrete.
## Instructor Notes
**Lesson goal:** Consolidate every command this course taught into ONE disciplined process,
directly matching the identical framework `root-10-m03-troubleshooting-framework` teaches at the
Kubernetes level — introduced here first, at the Linux level, so it already feels familiar by
then.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating this as a rigid checklist to recite rather than
an actual habit — the value only shows up when it's applied under a real, unfamiliar failure
(this module's Lab).
**Discussion question:** "Describe a time you fixed something without understanding WHY it
broke. Did it come back?" (Real personal examples land this lesson better than abstract
description.)
@@ -0,0 +1,64 @@
# Demonstration: Every Step, In Order, On a Real Failure
## OBSERVE
A startup script fails: `/srv/demo/start.sh` exits with an error. That's the specific, concrete
symptom — not "the service is down."
## GATHER EVIDENCE
```
$ /srv/demo/start.sh
FATAL: config file not found: /srv/demo/config/settings.conf
```
The script's own error message IS the evidence — no guessing required.
## FORM HYPOTHESIS
The config file at that exact path doesn't exist. That's the specific, testable hypothesis —
not "something's misconfigured."
## TEST
```
$ ls /srv/demo/config/
ls: cannot access '/srv/demo/config/': No such file or directory
```
Confirmed — the directory itself doesn't exist yet, not just the file.
## REPAIR
```
$ mkdir -p /srv/demo/config
$ echo "mode=production" > /srv/demo/config/settings.conf
```
The minimal fix for the CONFIRMED cause — not a broader "reinstall everything" overcorrection.
## VALIDATE
```
$ /srv/demo/start.sh
Started with config: mode=production
```
The original symptom is gone, confirmed with real output, not assumption.
## PREVENT RECURRENCE
A real prevention step here: the script should probably check for and create its config
directory on first run, or the deployment process should ensure the config exists before the
service ever starts — naming this explicitly is what separates a real fix from a lucky one.
## Instructor Notes
**Demo goal:** Show all seven steps applied in strict order to one concrete, realistic failure
— the exact structure this module's Lab requires students to repeat themselves.
**Approximate timing:** 10 minutes live.
**Common mistake to watch for:** skipping straight to REPAIR (creating the file) without first
articulating the hypothesis and testing it — even when the fix seems obvious, naming the
hypothesis explicitly is the habit this module is building.
@@ -0,0 +1,41 @@
# Knowledge Check: Linux Troubleshooting
---
**1. What's the difference between OBSERVE and GATHER EVIDENCE?**
*Answer:* OBSERVE names the specific symptom (what actually failed, precisely). GATHER EVIDENCE
collects the raw facts (logs, config, process/port state) needed to understand WHY, before
guessing at a cause.
---
**2. Why does FORM HYPOTHESIS come before TEST, rather than jumping straight to REPAIR?**
*Answer:* A hypothesis is a specific, falsifiable guess at the cause based on evidence already
gathered — testing it BEFORE repairing confirms you're about to fix the right thing, rather than
making a change that might not address the actual cause at all.
---
**3. A fix works, but the same failure happens again a week later. Which framework step was
most likely skipped?**
*Answer:* PREVENT RECURRENCE — the immediate symptom was resolved, but whatever allowed the
underlying cause to occur in the first place was never addressed.
---
**4. Why is "repair the minimal confirmed cause" better than "try several things until it
works"?**
*Answer:* A minimal, confirmed repair means you actually know what fixed it and can prevent
recurrence deliberately. Trying multiple things at once means even success leaves you unable to
say WHICH change mattered — and unable to reliably prevent the same failure next time.
## Instructor Notes
**Facilitation:** Question 3 is a good real-world check-in — ask if anyone has a personal
example of a fix that didn't actually stick.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,30 @@
# Guided Exercise: Write Your Hypothesis Before You Look at the Answer
This is a paper exercise — commit to a full framework pass before touching the Lab.
## Your task
You're told: "A script that's supposed to write a `ready` marker file isn't creating it, and
exits with a non-zero code." Before running anything, write out, specifically:
1. What's your OBSERVE statement (the precise symptom, not "it's broken")?
2. What EVIDENCE would you gather first, and with what command?
3. What's ONE specific, testable hypothesis (not "something's wrong with it")?
## A worked shape to check yourself against
1. Observe: "Script X exits non-zero and does not create file Y."
2. Evidence: redirect both stdout and stderr to a file and read it —
`script.sh > log 2>&1; cat log` — the script's own error output is almost always the fastest
evidence source for a startup failure.
3. Hypothesis (once you've actually read the error): something SPECIFIC the error names —
e.g. "the script requires a file at path X that doesn't currently exist" — not a vague
restatement of the symptom.
## Instructor Notes
**Facilitation:** The value here is entirely in getting students to write a hypothesis
SPECIFIC and FALSIFIABLE enough that they could be proven wrong — "something's misconfigured"
fails that bar; "the config file at this exact path is missing" passes it.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,31 @@
# Reflection: This Is the Same Framework You'll Use for the Rest of ROOT_
## Reflection questions
1. Which framework step did you personally find easiest to skip or rush through in this
module's Lab? Why do you think that step specifically tempts shortcuts?
2. You've now used this exact seven-step framework — Observe, Gather Evidence, Form Hypothesis,
Test, Repair, Validate, Prevent Recurrence — at the Linux level. Where do you expect to use
it again?
## Real Ops takeaway
**This entire course, from Module 1's five-question service checklist through Module 5's
failure-category taxonomy, was building toward this one repeatable discipline.** You'll meet
this exact framework again, formally named, in ROOT 10 — applied to Kubernetes instead of raw
Linux. Having already practiced it here means ROOT 10 won't be teaching you a new skill, just
retargeting a skill you already have.
This module — and ROOT 02 — is complete. Continue to ROOT 03: Git & Collaborative Development.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 tends to surface "Prevent Recurrence"
and "Form Hypothesis" (jumping straight to a guessed fix) as the two most commonly admitted
shortcuts — worth normalizing as a real, common temptation rather than a personal failing.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced writing.
**Milestone note:** this is the last module of ROOT 02 — consider a short recap connecting all
six modules (services → storage → networking model → inspection tools → DNS/HTTP → framework)
before moving to ROOT 03.
@@ -0,0 +1,40 @@
# Why Version Control Exists
## The problem: `final.txt`, `final_v2.txt`, `final_v2_ACTUALLY_FINAL.txt`
Before version control, "keeping history" usually meant copying a file with a new name every
time something changed. This fails fast: you lose track of WHAT changed between versions, WHO
changed it, and WHY — and it gets completely unworkable the moment more than one person touches
the same project.
## What version control actually gives you
- **History** — every change, in order, permanently. You can see exactly what a file looked
like at any point in the past.
- **Reproducibility** — you can recreate the EXACT state of an entire project at any past
moment, not just one file.
- **Collaboration** — multiple people can work on the same project without emailing files back
and forth or overwriting each other's work.
- **A source of truth** — one authoritative history everyone agrees is correct, instead of
"whoever has the newest-looking filename."
- **Rollback** — if a change turns out to be wrong, you can return to a known-good prior state
with confidence, not by hoping you kept a backup copy somewhere.
## Why copying files isn't version control
Copying `app.py` to `app_backup.py` before making a risky change LOOKS like version control, but
it has none of version control's real properties: no record of WHY the change was made, no way
to see incremental history, no way to combine two people's simultaneous edits, and no
scalability past a handful of files. Git (and version control generally) tracks CHANGES over
time as a structured history, not disconnected snapshots you have to manually manage.
## Instructor Notes
**Lesson goal:** Motivate Git before teaching a single command — students should feel the real
problem (the `final_v2_ACTUALLY_FINAL.txt` pattern) before the solution makes sense.
**Approximate timing:** 10 minutes read; 8 minutes live.
**Discussion question:** "Has anyone ever lost work, or overwritten someone else's work, because
of exactly this kind of file-copying problem?" (Real personal stories land this lesson far
better than abstract description.)
@@ -0,0 +1,31 @@
# Knowledge Check: Why Version Control Exists
---
**1. Name three real properties version control gives you that copying files as backups
doesn't.**
*Answer:* Any three of: structured history of every change, reproducibility of exact past
states, real collaboration between multiple people, a single agreed-upon source of truth,
confident rollback to a known-good state.
---
**2. Why does `final_v2_ACTUALLY_FINAL.txt` fail as a version-control strategy?**
*Answer:* It has no record of what changed or why between versions, doesn't scale past a
handful of files, and provides no way to merge two people's simultaneous changes.
---
**3. What does "source of truth" mean in the context of version control?**
*Answer:* One authoritative, agreed-upon history of a project that everyone treats as
correct — as opposed to ambiguity over which copy or filename is the "real" current version.
## Instructor Notes
**Facilitation:** Keep this brief — it's checking motivation/vocabulary before the hands-on
modules begin, not testing depth yet.
**Approximate timing:** 4 minutes.
@@ -0,0 +1,25 @@
# Reflection: Naming the Problem You're About to Solve
## Reflection questions
1. Have you personally ever lost work, or been confused about which file version was current,
in a way version control would have prevented?
2. What do you expect to feel DIFFERENT about how you work once you have real history and
rollback available?
## Real Ops takeaway
**Every engineering practice this program builds toward — code review, CI/CD, GitOps,
infrastructure as code — assumes version control as its foundation.** You cannot meaningfully
automate deployment, review changes before they ship, or declare infrastructure as code without
first having a reliable, structured history of what changed and when. This module named the
problem; the rest of ROOT 03 gives you the tool.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 tends to produce genuine, motivating
stories — let students share before moving to Module 2's mental model.
**Approximate timing:** 5 minutes live discussion; 3 minutes self-paced writing.
@@ -0,0 +1,59 @@
# Git Mental Model
## Three places your files can be
Git tracks your project through three distinct areas, and understanding this model is what
makes every Git command make sense — memorizing commands without it just produces confusion the
first time something doesn't behave as expected.
```
Working Tree → Staging Area → Repository (commit history)
(your actual (what you've (permanent,
files, as you marked ready recorded
edit them) to commit) snapshots)
```
- **Working tree** — the actual files on disk, exactly as you're editing them right now.
- **Staging area** (also called "the index") — a holding area for changes you've deliberately
marked as ready to be committed. This is Git's most distinctive design choice: you don't
commit "everything that changed," you commit "exactly what you staged."
- **Repository** — the permanent, recorded history of every commit ever made — this is what
`git log` shows you.
## Why `git add` exists at all
A change starts in your working tree. `git add <file>` moves it (conceptually — the mechanics
are more subtle, but this framing is enough for now) into the staging area. `git commit` takes
EVERYTHING currently staged and permanently records it as a new snapshot in the repository's
history. This two-step process (stage, then commit) exists so you can build up a commit
DELIBERATELY — staging exactly the related changes that belong together, even if your working
tree currently has other, unrelated changes you're not ready to commit yet.
## Core vocabulary
- **Commit** — one permanently recorded snapshot, with a message describing what changed and
why, and a unique identifier (a hash).
- **Branch** — a movable pointer to a specific commit, letting you develop something (a
"feature branch") without affecting the main line of history until you're ready to combine it
back in (Module 4).
- **HEAD** — Git's pointer to "which commit you're currently looking at" — almost always the
tip of whichever branch you have checked out.
- **Remote** — a reference to another copy of the same repository, usually hosted somewhere else
(Module 5).
## Instructor Notes
**Lesson goal:** Establish the three-area model BEFORE any commands — same "model before syntax"
pedagogy as ROOT 01's process model and ROOT 05's Kubernetes mental model. Every subsequent Git
command should be explainable in terms of moving something between these three areas.
**Approximate timing:** 15 minutes read; 10 minutes live, ideally with a literal diagram drawn
live (whiteboard or shared screen) showing files moving between the three areas.
**Common misconception to watch for:** assuming `git commit` commits every changed file
automatically — this is the single most common Git surprise for beginners, and it's exactly
what Module 3's Lab will make concrete.
**Discussion question:** "Why would you ever WANT to stage only some of your changes, not all
of them?" (Answer: to build one commit about ONE logical change, even if your working tree
currently has several unrelated edits in progress.)
@@ -0,0 +1,55 @@
# Demonstration: Watching a File Move Between the Three Areas
## OBSERVE
```
$ echo "draft" > report.txt
$ git status --short
?? report.txt
```
`??` means: Git sees this file, but it's in neither the staging area nor the repository yet —
purely in the working tree.
## HYPOTHESIS
Predict what `git status` will show after `git add report.txt`.
## INSPECT
```
$ git add report.txt
$ git status --short
A report.txt
```
`A` (in the LEFT column, which represents the staging area) means: staged, ready to commit.
## CHANGE
```
$ git commit -m "Add report draft"
$ git status --short
(nothing — clean)
```
## VERIFY
```
$ git log --oneline -1
a1b2c3d Add report draft
```
The file traveled through all three areas exactly as the model predicts: working tree (`??`) →
staging area (`A`) → repository (a permanent commit in `git log`).
## Instructor Notes
**Demo goal:** Make `git status`'s output legible by directly connecting its symbols (`??`, `A`,
clean) to the three-area model just taught, rather than treating them as arbitrary codes to
memorize.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** students reading `git status` output without connecting it
back to WHICH of the three areas each symbol refers to — reinforce the mapping explicitly.
@@ -0,0 +1,36 @@
# Knowledge Check: Git Mental Model
---
**1. Name the three areas a file passes through, in order.**
*Answer:* Working tree → staging area → repository (commit history).
---
**2. What does `git add` actually do, in terms of this model?**
*Answer:* Moves a change from the working tree into the staging area — marking it as ready to
be included in the next commit.
---
**3. If you edit three files but only `git add` one of them, what happens when you `git
commit`?**
*Answer:* Only the staged file is included in the new commit — the other two edited-but-unstaged
files remain as uncommitted changes in the working tree.
---
**4. What is HEAD?**
*Answer:* Git's pointer to the commit you're currently looking at — almost always the tip of
whichever branch you have checked out.
## Instructor Notes
**Facilitation:** Question 3 is the single most important check in this module — if students
get this wrong, revisit the diagram before continuing to Module 3.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,31 @@
# Guided Exercise: Predict `git status` Before You See It
This is a paper exercise — commit to a prediction before checking it in the Lab.
## Your task
Starting from a clean repository (nothing staged, nothing modified), predict what `git status
--short` would show after EACH of these actions, in sequence:
1. `echo "v1" > file.txt` (a brand new file)
2. `git add file.txt`
3. `echo "v2" >> file.txt` (modifying the ALREADY-STAGED file further)
4. `git commit -m "Add file"`
## A worked shape to check yourself against
1. `?? file.txt` — untracked, in the working tree only.
2. `A file.txt` — staged (left column).
3. `AM file.txt` — this is the surprising one: staged (`A`, left column) AND modified again
since staging (`M`, right column) — the staging area has a SNAPSHOT from step 2, and your
further edit in step 3 hasn't been staged yet.
4. Clean (nothing) — but only the STAGED version (from step 2, without the step-3 edit) actually
got committed; the step-3 edit remains as an uncommitted modification afterward.
## Instructor Notes
**Facilitation:** Item 3 is deliberately the hardest — it's the exact scenario that surprises
real beginners ("I thought I already staged this file, why does it show changes again?"). Worth
slowing down on.
**Approximate timing:** 10-12 minutes live discussion; 8 minutes self-paced.
@@ -0,0 +1,25 @@
# Reflection: The Model Explains the Commands, Not the Other Way Around
## Reflection questions
1. Before this module, would `git add` before `git commit` have seemed like an unnecessary
extra step? Why does it actually matter now?
2. Which of the three areas (working tree, staging, repository) do you feel least confident
distinguishing? What would help solidify it?
## Real Ops takeaway
**Every Git confusion you'll ever hit traces back to one of these three areas.** "Why isn't my
change in the commit," "why does `git status` show something weird," "why did my commit include
something I didn't mean to" — all of them are answerable by asking "which of the three areas is
this change actually in right now?" You now have that question available; Module 3 makes it
muscle memory.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 is worth a quick poll — knowing where
the class is least confident tells you where to slow down in Module 3's hands-on Lab.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,70 @@
# Everyday Git
## The commands you'll actually use constantly
Module 2 gave you the model. This module gives you the six commands that make up the vast
majority of real, everyday Git usage.
## `git init` — start tracking a project
```
$ git init
```
Creates a new, empty repository in the current directory (a hidden `.git` folder holds
everything). You only run this ONCE per project, at the very beginning.
## `git status` — where do things stand right now?
Shows what's staged, what's modified-but-unstaged, and what's untracked — the single most
useful command for orienting yourself, and one you'll run constantly, almost reflexively,
before and after almost every other Git command.
## `git add` — stage a change
```
$ git add file.txt # stage one specific file
$ git add . # stage everything currently changed
```
## `git commit` — permanently record what's staged
```
$ git commit -m "Add configuration loader"
```
A good commit message describes WHAT changed and, ideally, WHY — future-you (or a teammate)
reading `git log` months later should understand the change without re-reading the entire diff.
## `git log` — see the history
```
$ git log --oneline
a1b2c3d Add configuration loader
9f8e7d6 Initial commit
```
## `git diff` — see exactly what changed, before you commit it
```
$ git diff # unstaged changes vs. the last commit
$ git diff --staged # staged changes vs. the last commit
```
Reading your own diff before committing is one of the highest-value habits in this entire
course — catching an accidental leftover debug line, or a change you didn't mean to include,
BEFORE it becomes a permanent part of history.
## Instructor Notes
**Lesson goal:** Convert Module 2's model into the six commands used in nearly every real Git
session — this is the fluency floor for everything that follows in ROOT 03.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating `git add .` as always safe — it stages
EVERYTHING changed, including files you might not have intended to include; `git status` before
`git add .` is the habit that prevents this.
**Demo idea:** run the full init→status→add→commit→log→diff sequence live, narrating `git
status`'s changing output at each step.
@@ -0,0 +1,56 @@
# Demonstration: The Full Cycle, End to End
## OBSERVE
```
$ git init
$ git status --short
(nothing — empty repository)
```
## HYPOTHESIS
Predict what happens to `git status` after creating a new file.
## INSPECT
```
$ echo "v1" > app.py
$ git status --short
?? app.py
```
## CHANGE
```
$ git add app.py
$ git diff --staged
+v1
$ git commit -m "Add initial app.py"
```
`git diff --staged` shows exactly what's ABOUT to be committed — a real, final check before it
becomes permanent.
## VERIFY
```
$ git log --oneline
3f9a1c2 Add initial app.py
$ git status --short
(clean)
```
The complete cycle, verified with real evidence at each step — not "I think I committed it,"
but confirmed via `git log`.
## Instructor Notes
**Demo goal:** Show `git diff --staged` as the final safety check before a commit becomes
permanent — a habit worth building explicitly, not just mentioning in passing.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** skipping `git diff`/`git diff --staged` entirely and committing
on faith — a good moment to ask "how would you know if you accidentally staged the wrong
thing?"
@@ -0,0 +1,39 @@
# Knowledge Check: Everyday Git
---
**1. What's the difference between `git diff` and `git diff --staged`?**
*Answer:* `git diff` shows unstaged changes (working tree vs. last commit); `git diff --staged`
shows what's currently staged and about to be committed (staging area vs. last commit).
---
**2. Why is `git add .` potentially risky compared to `git add <specific-file>`?**
*Answer:* It stages EVERYTHING currently changed in the directory, which can accidentally
include files you didn't mean to commit — checking `git status` first (or using targeted `git
add`) avoids this.
---
**3. What makes a commit message actually useful months later?**
*Answer:* Describing WHAT changed and ideally WHY, clearly enough that someone (including
future-you) reading `git log` understands the change without re-reading the full diff.
---
**4. You run `git init` in a directory that already has an existing Git repository. What
happens?**
*Answer:* `git init` only needs to run once per project — running it again in an already-
initialized repository is harmless (it won't destroy existing history) but is never actually
necessary.
## Instructor Notes
**Facilitation:** Question 2 is a good moment to establish "check status before staging broadly"
as a house habit for the rest of the course.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,32 @@
# Guided Exercise: Plan a Real Commit Before You Make It
This is a paper exercise — commit to a plan before working in the Lab.
## Your task
You've made three unrelated changes to your working tree: fixed a typo in `README.md`, added a
new function to `utils.py`, and started (but not finished) an unrelated experiment in
`scratch.py` you're not ready to commit. Write out the exact sequence of commands you'd use to
end up with exactly ONE clean commit containing the README typo fix, leaving the other two files
alone.
## A worked shape to check yourself against
```
git status
git add README.md
git diff --staged
git commit -m "Fix typo in README"
git status
```
The key insight: `git add` lets you choose EXACTLY what goes into the next commit, regardless of
what else is sitting, unstaged, in your working tree — `utils.py` and `scratch.py` remain
untouched and uncommitted.
## Instructor Notes
**Facilitation:** This exercise directly tests whether Module 2's staging-area model actually
stuck — if students reach for `git add .`, that's a sign to revisit the model before the Lab.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,24 @@
# Reflection: You Have Real, Permanent History Now
## Reflection questions
1. Look at your own `git log` from this module's Lab. Is your commit message something a
stranger (or future-you) could understand without more context?
2. Which of the six commands from this module do you expect to type the most, going forward?
## Real Ops takeaway
**A real, permanent, inspectable history of your work now exists — this is the actual thing
Module 1 named as missing from copy-a-file-as-backup.** Every commit you make from here on is
recoverable, comparable, and explainable. Module 4 builds on this by letting multiple lines of
work (branches) develop independently before combining — impossible without the reliable
history you just built the habit of creating.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 works well as a peer-review pass —
have students read a neighbor's commit message and guess what it changed, then compare.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,62 @@
# Branching and Merging
## Why branches exist
Without branches, every commit happens on the same single line of history — risky work and
finished work are indistinguishable, and two people can't develop different things
simultaneously without directly interfering with each other. A **branch** is a movable pointer
to a commit, letting you develop something in isolation and only combine it back into the main
line when it's actually ready.
## Creating and switching branches
```
$ git branch feature-login # create a new branch (doesn't switch to it)
$ git checkout feature-login # switch to it
$ git switch feature-login # the newer, clearer equivalent of checkout for this purpose
$ git checkout -b feature-login # create AND switch in one step
```
Commits made while on `feature-login` only affect that branch — `main` is completely unaffected
until you deliberately combine them.
## Merging: combining two branches' history
```
$ git checkout main
$ git merge feature-login
```
Git attempts to combine the two branches' history automatically. When the two branches changed
DIFFERENT parts of a project, this usually succeeds with no fuss.
## Divergence and conflicts
When both branches changed the SAME lines of the SAME file differently, Git can't automatically
decide which version is correct — this is a **merge conflict**. Git marks the conflicting
section directly in the file:
```
<<<<<<< HEAD
your current branch's version
=======
the incoming branch's version
>>>>>>> feature-login
```
A conflict is not an error you did something wrong — it's Git correctly refusing to silently
guess which change you actually want. You resolve it by editing the file to the content you
actually want (removing the markers), then staging and committing to complete the merge.
## Instructor Notes
**Lesson goal:** Frame merge conflicts as an EXPECTED, normal part of collaborative work — not
a sign of failure — before students hit their first real one in this module's Lab.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** panicking at the sight of conflict markers — reinforce
explicitly that this is Git behaving correctly, not breaking.
**Demo idea:** deliberately narrate a conflict as "Git is asking YOU to make a decision it
can't make safely on its own" rather than "something went wrong."
@@ -0,0 +1,61 @@
# Demonstration: A Conflict, Caused and Resolved on Purpose
## OBSERVE
Two branches both changed line 1 of `config.txt` — `main` to `"env=production"`, `feature` to
`"env=staging"`.
## HYPOTHESIS
Predict what `git merge feature` will do.
## INSPECT
```
$ git merge feature
Auto-merging config.txt
CONFLICT (content): Merge conflict in config.txt
Automatic merge failed; fix conflicts and then commit the result.
```
Exactly as predicted — Git refuses to guess.
## CHANGE
```
$ cat config.txt
<<<<<<< HEAD
env=production
=======
env=staging
>>>>>>> feature
```
Decide the real answer — here, say `env=production` is correct — and edit the file to exactly
that, deleting all three marker lines.
## VERIFY
```
$ git add config.txt
$ git commit -m "Merge feature, keep production config"
$ git log --oneline --graph
* a1b2c3d Merge feature, keep production config
|\
| * 9f8e7d6 Update from feature
* | 3d2c1b0 Update from main
|/
* 0a1b2c3 Initial commit
```
The graph shows exactly what happened: two lines of history, deliberately combined.
## Instructor Notes
**Demo goal:** Walk a real conflict from cause to resolution without skipping any step,
reinforcing that resolution means making an explicit CHOICE, not applying a trick.
**Approximate timing:** 10 minutes live.
**Common mistake to watch for:** leaving conflict markers accidentally in the "resolved" file
(easy to miss one) — `grep '<<<<<<<' file` before committing catches this reliably.
@@ -0,0 +1,36 @@
# Knowledge Check: Branching and Merging
---
**1. What is a branch, really?**
*Answer:* A movable pointer to a specific commit — not a copy of the whole project, just a
reference letting you develop independently from other branches.
---
**2. Why does a merge conflict happen?**
*Answer:* Both branches changed the SAME lines of the SAME file differently, and Git can't
safely guess which version you actually want — so it stops and asks you to decide.
---
**3. Is a merge conflict a sign something went wrong?**
*Answer:* No — it's Git correctly refusing to silently guess at a decision only a human should
make. It's a normal, expected part of collaborative work.
---
**4. After resolving a conflict by editing the file, what two steps complete the merge?**
*Answer:* Stage the resolved file (`git add`) and commit (`git commit`) — exactly like
completing any other commit, once the conflict markers are gone.
## Instructor Notes
**Facilitation:** Question 3 is worth stating explicitly and repeating — reframing conflicts
as normal, not scary, meaningfully changes how confidently students approach this module's Lab.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,36 @@
# Guided Exercise: Decide the Resolution Before You Touch the File
This is a paper exercise — commit to a decision before resolving anything in the Lab.
## Your task
A merge conflict shows:
```
<<<<<<< HEAD
timeout_seconds = 30
=======
timeout_seconds = 120
>>>>>>> feature
```
You know `main`'s value (30) was set deliberately for production safety, but `feature`'s value
(120) was added while debugging a slow endpoint that genuinely needs more time. Write out: what
final content would you put in the file, and why is that the RIGHT resolution (not just "pick
one side")?
## A worked shape to check yourself against
A well-reasoned resolution here is neither side alone: something like keeping `30` as the
DEFAULT but adding a comment or a separate, more specific setting for the slow endpoint
specifically — the point is that conflict resolution sometimes means genuinely combining intent
from both sides, not mechanically picking `HEAD` or the incoming branch. There's no single
"correct" file content — the value is in justifying whichever choice you make with real
reasoning, not defaulting to whichever side happens to be `HEAD`.
## Instructor Notes
**Facilitation:** The point of this exercise is resisting "just pick a side" as a reflex —
some conflicts genuinely require combining or renegotiating intent, not mechanical selection.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,26 @@
# Reflection: Your First Conflict Wasn't a Disaster
## Reflection questions
1. Before this module, how would you have reacted to seeing conflict markers in a real file
for the first time?
2. What made resolving the conflict in this module's Lab feel manageable (or not)? What would
make a REAL conflict, in a large unfamiliar file, harder?
## Real Ops takeaway
**Merge conflicts are one of the most feared parts of Git for beginners, and one of the most
routine parts of Git for experienced engineers — the difference is just repetition.** You've now
caused one on purpose, resolved it deliberately, and verified the result — exactly the
experience that turns "conflicts are scary" into "conflicts are Tuesday." Module 5 adds a real
remote to this same workflow; conflicts show up there too, and you're now prepared for them.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 is worth extending into "what would
make a conflict on a 500-line file, with someone else's unfamiliar code, actually hard" — a good
honest preview of real collaborative work.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,56 @@
# Remote Repositories
## What a remote actually is
Every repository you've worked with so far has been entirely LOCAL — on this one machine. A
**remote** is a reference to another copy of the same repository, typically hosted somewhere
else (a server, or in this Lab, a second local repository playing that role). Remotes are what
make real collaboration possible: multiple people, multiple machines, all synchronizing through
one shared history.
## `origin`: a name, not a special concept
By convention, the remote you cloned FROM is named `origin` — but this is just a label Git
assigns automatically, not a technical requirement. You could name a remote anything; `origin`
is simply the default everyone recognizes.
## The four remote operations
- **`git clone <url>`** — copy an ENTIRE remote repository (full history included) to a new
local directory, automatically configuring it as `origin`.
- **`git fetch`** — download the remote's latest commits WITHOUT changing your own working
files or branches — a safe way to see what's new before deciding what to do about it.
- **`git pull`** — fetch AND immediately merge the remote's changes into your current branch —
effectively `fetch` + `merge` in one step (which is exactly why a `pull` can produce a merge
conflict, the same as Module 4's local merges).
- **`git push`** — upload your local commits to the remote, making them visible to everyone else
using that remote.
## Upstream: your branch's remote counterpart
A local branch can be configured to TRACK a specific remote branch (its "upstream") — once set,
plain `git pull`/`git push` (no extra arguments) know exactly which remote branch to sync
against, rather than requiring you to specify it every time.
## RootNode uses Gitea (this course does not)
RootNode's own engineering team uses Gitea (a self-hosted Git service) for its real work — the
CONCEPTS in this lesson (origin, clone, fetch, pull, push) are identical regardless of which
service hosts a remote (Gitea, GitHub, GitLab, or a private server). This course teaches those
concepts using a disposable LOCAL repository standing in for a remote — no account, no network
access, and no RootNode credentials are ever needed or exposed in this Lab.
## Instructor Notes
**Lesson goal:** Teach remotes as a genuine, transferable concept — students should be able to
work with ANY real Git hosting service afterward, not just this Lab's local simulation.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating `git pull` as a "safe, no-side-effects" command
— it's a merge, and merges can conflict, exactly like Module 4 taught.
**Discussion question:** "Why might a team prefer `git fetch` followed by manually reviewing
changes, over always running `git pull` directly?" (Answer: fetch lets you see what changed
before it touches your own working files/branch — useful caution before merging in unfamiliar
changes.)
@@ -0,0 +1,56 @@
# Demonstration: A Complete Clone-to-Push Round Trip
## OBSERVE
A bare (remote-style) repository already exists, currently empty.
```
$ git clone /srv/demo-remote.git /srv/demo-clone
warning: You appear to have cloned an empty repository.
```
Cloning an empty remote is completely normal — it just means no commits exist there yet.
## HYPOTHESIS
Predict what `git remote -v` will show inside the new clone.
## INSPECT
```
$ cd /srv/demo-clone && git remote -v
origin /srv/demo-remote.git (fetch)
origin /srv/demo-remote.git (push)
```
Automatically configured as `origin` — exactly as the lesson described, with no extra setup.
## CHANGE
```
$ echo "# Demo Project" > README.md
$ git add README.md
$ git commit -m "Add README"
$ git push origin main
```
## VERIFY
```
$ git --git-dir=/srv/demo-remote.git log --oneline
3f9a1c2 Add README
```
Checking the REMOTE directly (not the clone) proves the push actually reached it — the commit
now exists in two places, in sync.
## Instructor Notes
**Demo goal:** Prove the push actually worked by inspecting the REMOTE side independently,
not just trusting the clone's own `git log`.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** assuming a successful `git push` with no error message means
success without ever double-checking — worth reinforcing the "verify independently" habit from
earlier ROOT_ modules.
@@ -0,0 +1,40 @@
# Knowledge Check: Remote Repositories
---
**1. What's the difference between `git fetch` and `git pull`?**
*Answer:* `git fetch` downloads the remote's new commits without touching your working files or
branches. `git pull` does that AND immediately merges them into your current branch — it's
fetch + merge combined.
---
**2. Why can `git pull` produce a merge conflict?**
*Answer:* Because it performs a real merge under the hood — if your local branch and the
remote's branch both changed the same lines differently, the same conflict situation from
Module 4 can occur.
---
**3. Is `origin` a required, special name Git enforces?**
*Answer:* No — it's just the conventional default name Git assigns to the remote you cloned
from. You could name a remote something else; `origin` is a widely recognized convention, not a
technical requirement.
---
**4. Does this course require a GitHub or Gitea account to learn remotes?**
*Answer:* No — this course uses a disposable local bare repository to safely simulate a real
remote, teaching the same concepts (clone/fetch/pull/push/origin) without any account or network
access.
## Instructor Notes
**Facilitation:** Question 2 is worth emphasizing — many students assume `pull` is risk-free
until they hit their first real pull-conflict.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,30 @@
# Guided Exercise: Fetch First, Decide Second
This is a paper exercise — commit to a plan before working in the Lab.
## Your task
You're about to sync with a shared remote that a teammate has been actively pushing to all day.
You're not sure what they changed. Write out the SAFER sequence of commands (as opposed to
running `git pull` immediately), and explain what each step lets you verify before your own
working files are touched.
## A worked shape to check yourself against
```
git fetch origin
git log HEAD..origin/main --oneline
```
`git fetch` downloads the new commits without touching your branch. The `log` command then
shows you exactly what commits exist on the remote that you don't have locally yet — letting you
review WHAT changed before deciding to merge it in with a subsequent `git merge origin/main` (or
`git pull`, now that you already know roughly what to expect).
## Instructor Notes
**Facilitation:** This exercise directly builds the "fetch first, review, then merge" habit
real teams use on shared branches — worth connecting explicitly to why blind `git pull` on a
busy shared branch is a common source of surprise conflicts.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,24 @@
# Reflection: You Just Practiced Real Collaboration, Safely
## Reflection questions
1. What would have felt different about this module's Lab if the remote were a real hosted
service (Gitea/GitHub) instead of a local simulation? What would have felt the same?
2. Where do you expect `push` to matter most in real engineering work you'll do later?
## Real Ops takeaway
**Everything you just practiced — clone, commit, push, verified independently on the remote
side — is the exact mechanism behind CI/CD and GitOps, which ROOT 11 covers later.** A CI
pipeline triggers because someone pushed; a GitOps deployment happens because a change was
pushed to a Git repository treated as the source of truth. You haven't just learned Git commands
— you've practiced the literal trigger mechanism modern deployment automation is built around.
This module is complete. Continue to the final module of this course.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 previews ROOT 11's GitOps content
directly — worth naming that connection explicitly, even without teaching GitOps itself yet.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,54 @@
# Engineering Git Workflow
## Small commits, meaningful messages
A commit containing "fixed stuff" and 40 unrelated file changes is nearly useless to anyone
reading history later — including future-you. A commit that does ONE logical thing, with a
message describing what and why, is what actually makes `git log` a useful tool rather than
noise. This isn't pedantry — it's what makes rollback (below) and code review actually
practical.
## `main` as a protected source of truth
Real engineering teams treat their main branch as the one everyone trusts to be in a working
state at all times — direct, unreviewed pushes to `main` are typically restricted specifically
BECAUSE it's the shared source of truth every deployment and every other branch is built from.
Feature branches (Module 4) exist precisely so risky, in-progress work never threatens that
trust.
## Review: another set of eyes before history is permanent
Before a change reaches `main`, most real teams have someone else look at it first (a "pull
request" or "merge request," depending on the platform). This isn't about distrust — it's a
second perspective catching mistakes, and a natural point to discuss WHY a change is being made,
not just what it does.
## History and rollback as safety nets
Because every commit is permanent and inspectable, a bad change can be identified precisely (via
`git log`) and reverted deliberately — confident recovery, not panic, when something goes wrong.
This is Module 1's original promise, now fully realized: reproducibility and rollback aren't
theoretical, they're what your daily small-commit discipline actually buys you.
## Why this matters to DevOps, GitOps, and Infrastructure as Code
Everything in this lesson — small deliberate changes, a trusted source of truth, review before
merge, reliable history — is EXACTLY what CI/CD pipelines automate, what GitOps treats a Git
repository as authoritative for, and what Infrastructure as Code relies on to safely manage real
infrastructure through the same discipline you just practiced on plain text files. ROOT 11
builds directly on this course's habits, not a new set of ideas.
## Instructor Notes
**Lesson goal:** Close ROOT 03 by explicitly naming the professional discipline (not just Git
commands) this course was building, and its direct connection to DevOps/GitOps/IaC.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating "review" and "protecting main" as bureaucratic
overhead rather than genuine risk management — connect it back to the real cost of a bad change
reaching a shared source of truth unreviewed.
**Discussion question:** "Why might a team enforce small commits and review even when working
alone on a personal project?" (Good answers: future-you benefits from the same clarity; building
the habit now pays off the moment you DO work with others.)
@@ -0,0 +1,47 @@
# Demonstration: The Same Change, Told Two Ways
## OBSERVE
```
$ git log --oneline
a1b2c3d fixed stuff
```
One commit, an unhelpful message, and (imagine) 15 unrelated files changed together: a bug fix,
a typo correction, and an unrelated half-finished experiment.
## HYPOTHESIS
Predict: six months from now, could you (or a teammate) confidently `git revert` just the bug
fix from this commit, leaving the other 14 files untouched?
## INSPECT
No — reverting this ONE commit reverts everything in it together, including the unrelated
typo fix and experiment, whether you want that or not.
## CHANGE
The same work, done as three separate, well-described commits instead:
```
$ git log --oneline
3f9a1c2 Fix null-pointer crash in payment retry logic
9d8e7f6 Fix typo in onboarding email template
1a2b3c4 WIP: experiment with new caching strategy (not ready)
```
## VERIFY
Now `git revert 3f9a1c2` cleanly undoes JUST the payment bug fix, with zero effect on the other
two — because each commit is one logical, independently-reversible unit of work.
## Instructor Notes
**Demo goal:** Make the practical cost of "fixed stuff" commits concrete — not a style
preference, a genuine loss of capability (precise revert, clear history, easier review).
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** treating this as a purely aesthetic preference rather than a
practical one — reinforce the concrete "can you revert just the bug fix" test.
@@ -0,0 +1,32 @@
# Knowledge Check: Engineering Git Workflow
---
**1. Why does bundling unrelated changes into one commit cost you something real, not just
style points?**
*Answer:* It makes precise operations like `git revert` impossible to apply to just ONE logical
change — reverting the commit reverts everything in it together, whether you want that or not.
---
**2. Why do teams typically restrict direct, unreviewed pushes to `main`?**
*Answer:* `main` is the shared source of truth everything else (deployments, other branches) is
built from — protecting it reduces the chance an unreviewed mistake reaches something everyone
else depends on being trustworthy.
---
**3. Name one concrete way this course's Git discipline connects to GitOps or CI/CD.**
*Answer:* Any reasonable answer connecting small deliberate changes / a trusted source of truth
/ review-before-merge / reliable history to how CI/CD pipelines trigger on pushes or how GitOps
treats a Git repository as the authoritative record of desired infrastructure state.
## Instructor Notes
**Facilitation:** Question 3 is open-ended by design — the goal is students making the
connection in their own words, not reciting a fixed answer.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,30 @@
# Reflection: You've Practiced Real Engineering Discipline, Not Just Syntax
## Reflection questions
1. Look back across all six modules of ROOT 03. Which single habit (small commits, checking
`git status` before staging, fetch-before-pull, resolving conflicts deliberately) do you
think will matter most in real work?
2. This course started with "copying files as backup isn't version control." Can you now
explain, in one sentence, what Git actually gives you that copying never could?
## Real Ops takeaway
**Version control discipline is not a Git-specific skill — it's an engineering discipline that
happens to be exercised through Git.** Small deliberate changes, a trusted shared source of
truth, review before permanent history, and confident rollback are the same principles behind
CI/CD, GitOps, and Infrastructure as Code, all covered later in ROOT 11. You are not starting
that content from zero — you already practiced its foundation here.
This module — and ROOT 03 — is complete. Continue to ROOT 04: Containers & Docker.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 is a good closing check — a clear,
confident answer here is real evidence the course's central premise landed.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced writing.
**Milestone note:** this is the last module of ROOT 03 — consider a brief recap connecting all
six modules (why version control → the three-area model → everyday commands → branching/
conflicts → remotes → professional workflow) before moving to ROOT 04.
@@ -0,0 +1,51 @@
# Why Containers Exist
## "It works on my machine"
Every engineer has heard this sentence, and it names a real, expensive problem: an application
that runs perfectly on one machine fails on another because of differences in installed
libraries, OS versions, configuration, or dozens of other environmental details that are easy to
overlook and hard to fully document.
## The problem, precisely
- **Dependency inconsistency** — an app needs Python 3.11 and a specific set of libraries; the
production server has Python 3.9 and different library versions.
- **Non-repeatability** — setting up a working environment by hand, following a README, drifts
over time and between people — "works on my machine" is often true because "my machine" has
accumulated years of undocumented manual setup.
- **Deployment friction** — moving an application from a developer's laptop to a test server to
production means repeating fragile manual setup multiple times, each an opportunity for drift.
## Containers: package the application AND everything it needs, together
A **container** packages an application with its exact dependencies, libraries, and
configuration into one deployable unit — the same container that ran on a developer's laptop
runs, byte-for-byte identically, on a test server or in production. "It works on my machine"
becomes "it works in this container," which is true everywhere that container runs.
## VM vs. container — genuinely different, not just "containers are lighter VMs"
A **virtual machine** virtualizes an entire computer, including its own full operating system
kernel — heavier, slower to start, but with very strong isolation. A **container** shares the
HOST machine's kernel and only isolates the application's process, filesystem view, and network
— dramatically lighter and faster to start (seconds vs. minutes), with somewhat different
isolation properties than a full VM (a well-understood, documented tradeoff, not a hidden flaw).
Neither is strictly "better" — they solve overlapping but distinct problems, and real production
systems often use both together (VMs providing strong host isolation, containers providing fast,
consistent application packaging on top).
## Instructor Notes
**Lesson goal:** Motivate containers through the real, felt problem ("it works on my machine")
before any Docker command — mirrors ROOT 03's "why version control exists" opener.
**Approximate timing:** 12 minutes read; 8 minutes live.
**Common misconception to watch for:** describing containers as simply "lightweight VMs" —
they share the host kernel, which is a genuine architectural difference, not just a size
difference.
**Discussion question:** "Has anyone experienced 'it works on my machine' personally, on either
side of that conversation?" (Real stories make this lesson's premise concrete before the
technical content begins.)
@@ -0,0 +1,49 @@
# Demonstration: What "Shares the Host Kernel" Actually Looks Like
## OBSERVE
```
$ uname -r
5.15.0-real-host-kernel
$ docker run --rm alpine:3.19 uname -r
5.15.0-real-host-kernel
```
## HYPOTHESIS
Predict: would a real virtual machine show the SAME kernel version as its host, the way this
container just did?
## INSPECT
A real VM runs its OWN independent kernel — it would very likely show a DIFFERENT version (or
even a different OS entirely) than its host, because a VM virtualizes the hardware and boots a
complete, separate operating system on top of it.
## CHANGE
```
$ docker run --rm ubuntu:22.04 uname -r
5.15.0-real-host-kernel
```
Even a completely different container OS (`ubuntu` here, vs. `alpine` before) STILL reports the
exact same kernel version — because there's only ONE real kernel involved, the host's.
## VERIFY
This is the concrete, observable meaning of "containers share the host kernel": no matter what
container image you run, `uname -r` always reports the same real kernel underneath — this is
exactly why containers start in a fraction of a second (no separate kernel to boot) and exactly
the tradeoff the lesson named.
## Instructor Notes
**Demo goal:** Make "shares the host kernel" a concrete, checkable fact rather than an abstract
architectural claim.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** assuming different container images run different kernels
because their USERSPACE (installed tools, package manager) looks completely different — the
kernel underneath is unified regardless.
@@ -0,0 +1,33 @@
# Knowledge Check: Why Containers Exist
---
**1. What real problem does "it works on my machine" name?**
*Answer:* Dependency/environment inconsistency between where an application was developed and
where it actually needs to run — differences in libraries, versions, and configuration that are
easy to overlook and hard to fully document by hand.
---
**2. What's the actual architectural difference between a container and a virtual machine, not
just "containers are smaller"?**
*Answer:* A container shares the host machine's kernel and only isolates the application's
process/filesystem/network view. A VM virtualizes an entire computer, including running its own
separate kernel — a genuinely different isolation model, not just a size difference.
---
**3. Why do containers start so much faster than VMs?**
*Answer:* A container doesn't boot a separate operating system/kernel — it starts a process
using the ALREADY-RUNNING host kernel, while a VM has to boot an entire independent OS from
scratch.
## Instructor Notes
**Facilitation:** Question 2 is the one worth confirming explicitly — many students carry the
misconception "container = tiny VM" past this point without correction.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,26 @@
# Reflection: You Now Know What Problem You're About to Solve, Hands-On
## Reflection questions
1. Think back to ROOT 01's "how computers run software" module. How does today's lesson refine
your understanding of what a "process" actually is?
2. Do you expect containers to make the Linux/process/networking skills from ROOT 01-02 less
relevant, or MORE relevant? Why?
## Real Ops takeaway
**Every skill from ROOT 01-02 — processes, filesystems, networking — transfers directly into
containers, because a container is built from exactly those same primitives, just isolated
differently.** You are not about to learn an unrelated new subject; you're about to see the
Linux fundamentals you already have applied through Docker's specific tooling. Module 2 starts
that translation.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2's honest answer is "more relevant" —
worth stating explicitly, since students sometimes assume containers are an alternative to
Linux fundamentals rather than built directly on top of them.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,51 @@
# Images and Containers
## The single most important distinction in this course
**An image is a packaged, read-only ARTIFACT. A container is a RUNNING INSTANCE of an image.**
This is exactly analogous to a program's executable file (the image) versus a running process
of that program (the container) — a distinction ROOT 01's process module already gave you
vocabulary for. You can run the SAME image many times, producing many independent containers,
the same way you can run the same program multiple times as separate processes.
## Layers: how an image is actually built
An image is built from a stack of **layers**, each representing one change (install a package,
copy in application code, set a configuration). Layers are cached and shared — if two images
both start `FROM alpine:3.19`, they share that base layer on disk rather than duplicating it,
which is why pulling a new image that shares layers with one you already have is often much
faster than the total image size suggests.
## Registries, repositories, and tags
- **Registry** — a server that stores and distributes images (Docker Hub is the default public
one; a company might run a private registry).
- **Repository** — a named collection of related images (e.g. `nginx`).
- **Tag** — a specific version within a repository (`nginx:alpine`, `nginx:1.25`, `nginx:latest`).
`latest` is just a conventional tag name, not automatically "the newest" in any enforced sense
— relying on it in production is a common, avoidable source of surprise.
## The core commands, mapped to the model
- **`docker pull <image>`** — download an image from a registry, without running it.
- **`docker run <image>`** — create and start a NEW container from an image (pulls automatically
if not already present locally).
- **`docker stop <container>`** — gracefully stop a running container (its process, not the
image).
- **`docker rm <container>`** — delete a stopped container. This does NOT delete the image it
was created from.
## Instructor Notes
**Lesson goal:** Land the image/container distinction as concretely as possible — nearly every
later confusion in this course traces back to conflating the two.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** using "image" and "container" interchangeably in casual
speech — gently correct this early and often, since precise vocabulary here prevents real
confusion later (e.g., "did you delete the container or the image?" matters).
**Discussion question:** "If you run `docker run nginx:alpine` three times, how many images
exist afterward? How many containers?" (Answer: 1 image, 3 containers — a good check before
moving to the hands-on Lab.)
@@ -0,0 +1,59 @@
# Demonstration: One Image, Many Independent Containers
## OBSERVE
```
$ docker images | grep alpine
alpine 3.19 a1b2c3d4e5f6 2 weeks ago 7.4MB
```
Exactly one `alpine:3.19` image exists locally right now.
## HYPOTHESIS
Predict: after running three containers from it, how many images will `docker images` show?
## INSPECT
```
$ docker run -d --name c1 alpine:3.19 sleep 300
$ docker run -d --name c2 alpine:3.19 sleep 300
$ docker run -d --name c3 alpine:3.19 sleep 300
$ docker images | grep alpine
alpine 3.19 a1b2c3d4e5f6 2 weeks ago 7.4MB
```
Still exactly one image — confirmed.
## CHANGE
```
$ docker ps --format '{{.Names}}: {{.ID}}'
c3: 9f8e7d6c5b4a
c2: 3d2c1b0a9f8e
c1: 7c6b5a4d3e2f
```
Three DIFFERENT containers, each with its own ID, all created from that same one image.
## VERIFY
```
$ docker rm -f c1 c2 c3
$ docker images | grep alpine
alpine 3.19 a1b2c3d4e5f6 2 weeks ago 7.4MB
```
Removing all three containers left the image completely untouched — proving containers and the
image they came from have independent lifecycles.
## Instructor Notes
**Demo goal:** Make the 1-image-many-containers relationship undeniable by actually counting
both before and after, rather than just stating it.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** assuming `docker rm` on a container also removes its image —
a good moment to reinforce that images and containers have separate removal commands
(`docker rm` for containers, `docker rmi` for images) for exactly this reason.
@@ -0,0 +1,38 @@
# Knowledge Check: Images and Containers
---
**1. State the image/container distinction in one sentence.**
*Answer:* An image is a packaged, read-only artifact; a container is a running instance created
from an image — the same relationship as a program file and a running process of it.
---
**2. You run `docker rm -f my-container`. Does this delete the image it was created from?**
*Answer:* No — removing a container never removes the image; they have independent lifecycles
and separate removal commands (`docker rm` vs. `docker rmi`).
---
**3. What does the `latest` tag actually guarantee?**
*Answer:* Nothing enforced — it's just a conventional tag name, not automatically "the newest
version" in any technical sense. Relying on it in production is a common source of surprise.
---
**4. What's a layer, and why does layer sharing matter practically?**
*Answer:* A layer is one recorded change in an image's build (installing something, copying
files, etc.). Layers are cached and shared between images that share a base — meaning pulling a
new image that shares layers with one you already have can be much faster than its total
reported size suggests.
## Instructor Notes
**Facilitation:** Question 2 directly reinforces the previous demo — worth confirming everyone
got it right before moving on.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,31 @@
# Guided Exercise: Trace the Counts Through a Sequence of Commands
This is a paper exercise — commit to your answer before checking it in the Lab.
## Your task
Starting from a completely clean system (no images, no containers), predict the number of
IMAGES and the number of CONTAINERS after each step:
1. `docker pull redis:alpine`
2. `docker run -d --name cache1 redis:alpine`
3. `docker run -d --name cache2 redis:alpine`
4. `docker rm -f cache1`
5. `docker rmi redis:alpine` (attempted while `cache2` is still running)
## A worked shape to check yourself against
1. 1 image, 0 containers.
2. 1 image, 1 container.
3. 1 image, 2 containers.
4. 1 image, 1 container (only `cache1` removed).
5. This actually FAILS — Docker refuses to remove an image that a running container (`cache2`)
still depends on, specifically to prevent exactly this kind of accident. Still 1 image, 1
container, plus an error message.
## Instructor Notes
**Facilitation:** Item 5 is the most valuable one — many students haven't seen Docker's
dependency-protection behavior yet, and it's a good real safety feature to highlight.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,24 @@
# Reflection: A Small Distinction With Large Consequences
## Reflection questions
1. Before this module, would you have known why `docker rm` and `docker rmi` are two separate
commands? Does it make sense now?
2. Where else in ROOT_ have you seen this exact "artifact vs. running instance" pattern before?
(Hint: think back to ROOT 01.)
## Real Ops takeaway
**Precise vocabulary prevents real mistakes.** Confusing "stop the container" with "delete the
image," or assuming `latest` means "the current version," are genuine, common sources of
production incidents — not just semantic nitpicking. The image/container distinction you just
solidified is the foundation every command in the rest of this course builds on.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2's intended answer is "program vs.
process," from ROOT 01 — a good moment to explicitly connect the two courses.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,67 @@
# Docker CLI
## The commands you'll use constantly
Module 2 introduced pull/run/stop/rm conceptually. This module adds the rest of the everyday
toolkit — and every one of them maps directly to a Linux concept you already have from ROOT 01.
## `docker ps` — what's running right now
```
$ docker ps
CONTAINER ID IMAGE STATUS NAMES
a1b2c3d4e5f6 nginx:alpine Up 2 minutes web
```
This is the container-level equivalent of ROOT 02's `ps aux` — "what's actually running." Add
`-a` to see stopped containers too, not just running ones.
## `docker logs` — what has it been saying?
```
$ docker logs web
```
The container-level equivalent of reading a service's log file from ROOT 02 — except Docker
captures a container's stdout/stderr automatically, so there's no config needed to find WHERE
the logs live, unlike a traditional service.
## `docker exec` — run a NEW command inside an ALREADY-running container
```
$ docker exec web sh -c 'cat /etc/nginx/nginx.conf'
```
Different from `docker run`, which creates a brand-new container — `exec` reaches INTO an
existing, running one, exactly like ROOT 01's `ps`/inspection commands but scoped to one
specific container's process namespace.
## `docker inspect` — the full, detailed truth about a container
```
$ docker inspect web --format '{{.State.Running}}'
true
```
Returns a large JSON document with everything Docker knows about a container — `--format` lets
you extract just the one field you care about, rather than reading the whole thing.
## `docker stop` vs `docker rm`
`docker stop` gracefully stops a running container (sends SIGTERM, then SIGKILL after a grace
period — literally ROOT 01's process-signals lesson, applied to a container's main process).
`docker rm` deletes a STOPPED container entirely — `docker rm -f` combines both (force-stop then
remove) in one step.
## Instructor Notes
**Lesson goal:** Explicitly map every Docker CLI command back to the equivalent ROOT 01/02
Linux concept students already have — this is a translation exercise, not new material.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** confusing `docker run` (creates a new container) with
`docker exec` (runs a command in an existing one) — a very common early mixup.
**Demo idea:** run `docker exec` against a container, then immediately `docker run` the same
image again, and compare `docker ps` before/after to show the difference concretely.
@@ -0,0 +1,62 @@
# Demonstration: `run` Creates, `exec` Reaches In
## OBSERVE
```
$ docker run -d --name web nginx:alpine
$ docker ps --format '{{.Names}}'
web
```
One container exists.
## HYPOTHESIS
Predict: will `docker exec web echo hello` create a SECOND container?
## INSPECT
```
$ docker exec web echo hello
hello
$ docker ps --format '{{.Names}}'
web
```
Still exactly one container — `exec` ran a new command INSIDE the existing one, it never
created anything new.
## CHANGE
Now actually run the same image again:
```
$ docker run -d --name web2 nginx:alpine
$ docker ps --format '{{.Names}}'
web2
web
```
Now two containers exist — because `run`, unlike `exec`, always creates a NEW container.
## VERIFY
```
$ docker logs web | tail -3
$ docker inspect web --format '{{.State.Running}}'
true
```
Both containers are independent, each with their own logs and state — exactly as Module 2's
"one image, many containers" demonstration showed.
## Instructor Notes
**Demo goal:** Directly contrast `run` and `exec`'s effect on the container COUNT — this is the
most common early confusion and deserves a side-by-side comparison.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** trying to `exec` into a container that isn't running yet
(`exec` requires an ALREADY-running target) — a good moment to connect back to `docker ps`
first.
@@ -0,0 +1,39 @@
# Knowledge Check: Docker CLI
---
**1. What's the real difference between `docker run` and `docker exec`?**
*Answer:* `docker run` always creates a brand-new container from an image. `docker exec` runs a
new command inside an ALREADY-running container — no new container is created.
---
**2. Which command is the container-level equivalent of ROOT 02's "what's running" question
(`ps aux` / service inspection)?**
*Answer:* `docker ps` (add `-a` to include stopped containers too).
---
**3. Why doesn't `docker logs` require you to know a specific log file path, unlike a
traditional Linux service?**
*Answer:* Docker automatically captures a container's stdout/stderr output — there's no
separate log file configuration needed the way ROOT 02's services required.
---
**4. What does `docker stop` actually send to a container's main process, and why does that
matter?**
*Answer:* SIGTERM first (a polite shutdown request, giving the process a chance to clean up),
then SIGKILL after a grace period if it hasn't exited — the exact same signal discipline ROOT 01
taught for regular Linux processes, applied here to a container's main process.
## Instructor Notes
**Facilitation:** Question 1 is the single most important check in this module — revisit the
demo if students get it wrong.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,29 @@
# Guided Exercise: Which Docker Command Actually Does This?
This is a paper exercise — commit to an answer before checking it in the Lab.
## Your task
For each goal, name the ONE Docker command you'd use:
1. "See every container, including ones that have stopped."
2. "Run a one-off diagnostic command inside a container that's already running."
3. "Read what a container has printed to its logs so far."
4. "Check the exact value of one specific field (like whether it's currently running) without
reading the entire inspect output."
5. "Completely remove a running container in one step, without stopping it first separately."
## A worked shape to check yourself against
1. `docker ps -a`
2. `docker exec <container> <command>`
3. `docker logs <container>`
4. `docker inspect <container> --format '{{...}}'`
5. `docker rm -f <container>`
## Instructor Notes
**Facilitation:** Have students justify why each answer fits the described GOAL specifically,
not just recite familiar syntax.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,24 @@
# Reflection: The Whole Toolkit Was Already Familiar
## Reflection questions
1. Which Docker command in this module felt like "a new version of something I already knew"
from ROOT 01 or ROOT 02? Which felt genuinely new?
2. You ran, inspected, execed into, and removed a real container this module. What's the ONE
command you'd reach for first if a container "seemed broken" and you didn't know why yet?
## Real Ops takeaway
**Every Docker command in this module is a direct translation of a Linux skill you already
had.** `docker ps` is `ps aux`. `docker logs` is reading a log file, minus the config. `docker
exec` is reaching into a running process. This is exactly why ROOT 01-02 came before ROOT 04 —
containers don't replace Linux fundamentals, they wrap them in a specific, consistent interface.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2's honest answer is usually `docker ps`
or `docker logs` — a good moment to connect back to ROOT 02's five-question service checklist.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,58 @@
# Container Networking
## This module is ROOT 02's networking model, one layer up
Every concept here is a direct application of ROOT 02 Module 3's host-to-application chain —
just with a container standing in for "host."
## Every container gets its own IP
By default, Docker gives each container its own IP address on a private, internal Docker
network — exactly like a real host has an IP on a real network (ROOT 02 Module 3). Containers on
the SAME Docker network can reach each other directly by that IP.
## Container-to-container DNS: reach a container by NAME
Docker runs its own internal DNS for user-defined networks — containers on the same network can
resolve each other by CONTAINER NAME, not just IP. If a `web` container needs to reach an `api`
container, it can simply connect to hostname `api` — Docker resolves it automatically. This is
the exact "getent hosts" concept from ROOT 02, just automated for you.
## Publishing a port: the host/container boundary
A container's ports are, by default, only reachable from OTHER containers on the same Docker
network — NOT from the host machine, and NOT from outside. **Publishing** a port
(`-p hostPort:containerPort`) explicitly creates a mapping: traffic arriving at the HOST's
`hostPort` gets forwarded to the CONTAINER's `containerPort`. This is set at container CREATION
time — you cannot add it to an already-running container; it must be recreated.
```
docker run -d -p 8080:80 nginx:alpine
```
Host port 8080 → this container's port 80. `curl http://localhost:8080/` from the host now
reaches it; `curl http://localhost:80/` from the host does NOT (the host's own port 80 was never
touched).
## A working app that's still unreachable is a real, common failure
Exactly like ROOT 02's networking-diagnosis modules: a container can be perfectly healthy
internally (`docker exec <container> curl localhost:80` succeeds) while being completely
unreachable from the host, because publishing was never configured. This is one of the single
most common container networking mistakes — and this module's Lab makes you diagnose and fix
it for real.
## Instructor Notes
**Lesson goal:** Explicitly connect every idea here back to ROOT 02's networking model — this
module should feel like recognition, not new material, reinforcing the curriculum's continuity.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** assuming a container's port is automatically reachable
from the host just because the container itself works fine — this exact misconception is what
this module's Lab is built to correct through direct experience.
**Discussion question:** "Why might Docker default to NOT exposing container ports to the host
automatically?" (Good answer: explicit opt-in is safer — you don't want every container's every
port accidentally reachable from outside by default.)
@@ -0,0 +1,51 @@
# Demonstration: Reaching a Container By Name, Not IP
## OBSERVE
```
$ docker network create demo-net
$ docker run -d --name api-server --network demo-net nginx:alpine
```
## HYPOTHESIS
Predict: can another container on `demo-net` reach `api-server` using its NAME as a hostname,
without knowing its IP?
## INSPECT
```
$ docker run --rm --network demo-net alpine:3.19 sh -c "wget -qO- http://api-server:80/ | head -1"
<!DOCTYPE html>
```
Yes — Docker's built-in DNS resolved `api-server` (a container NAME) directly, no IP address
needed anywhere in that command.
## CHANGE
Now try reaching it from a container that is NOT on the same network:
```
$ docker run --rm alpine:3.19 sh -c "wget -qO- http://api-server:80/ 2>&1 | head -1"
wget: bad address 'api-server'
```
Name resolution fails entirely — the container isn't even on the same Docker network, so there's
no DNS entry for it there.
## VERIFY
Container-to-container name resolution is scoped to a SPECIFIC Docker network, not global —
exactly like ROOT 02's subnet concept: reachability depends on which network you're actually on.
## Instructor Notes
**Demo goal:** Show container-name DNS resolution as a real, working feature — and show it
explicitly FAILING when containers aren't on the same network, reinforcing that this isn't
magic, it's scoped.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** assuming any two containers can always reach each other by name
regardless of network — reinforce the "same network required" rule explicitly.
@@ -0,0 +1,40 @@
# Knowledge Check: Container Networking
---
**1. A container works fine when tested with `docker exec <container> curl localhost:80`, but
`curl http://localhost:8080/` from the host fails. What's the most likely cause?**
*Answer:* The container was never started with a port-publish flag (`-p 8080:80`) mapping the
host port to the container's port — the app works internally, but nothing connects the host to
it.
---
**2. Can you add port publishing to an already-running container?**
*Answer:* No — port publishing is set at container creation time; you must remove and recreate
the container with the correct `-p` flag.
---
**3. How can one container reach another by name instead of IP?**
*Answer:* If both containers are on the same user-defined Docker network, Docker's built-in DNS
resolves the target container's NAME automatically — no IP address needed.
---
**4. Why does container-name resolution fail between two containers that AREN'T on the same
Docker network?**
*Answer:* Docker's per-network DNS only knows about containers actually attached to that
specific network — being on a different (or no) network means there's no DNS entry for the
target container at all.
## Instructor Notes
**Facilitation:** Question 1 directly previews this module's Lab — a good moment to build
anticipation for it.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,29 @@
# Guided Exercise: Diagnose Before You Recreate Anything
This is a paper exercise — commit to a diagnostic plan before working in the Lab.
## Your task
A container `payments-api` is reported as "unreachable" by a teammate. Before touching anything,
write out, in order, the exact commands you'd run to determine WHERE the problem actually is —
using this module's and ROOT 02's tools.
## A worked shape to check yourself against
```
docker ps --filter name=payments-api # is it even running?
docker exec payments-api curl -sf localhost:PORT/ # does it work internally?
docker port payments-api # is anything actually published?
curl -sf http://localhost:PUBLISHED_PORT/ # does the published mapping actually work?
```
This sequence isolates each layer independently — running? working internally? published at
all? published mapping actually functional? — exactly the same "isolate one variable at a time"
discipline ROOT 02 Module 3 taught, applied here to containers specifically.
## Instructor Notes
**Facilitation:** This exercise is deliberately a direct copy of ROOT 02's diagnostic discipline
— the value is students recognizing that connection themselves, not being told it explicitly.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,25 @@
# Reflection: Same Diagnosis, New Layer
## Reflection questions
1. How much of this module's Lab did you solve using pure Docker knowledge, versus reasoning
you already had from ROOT 02?
2. Where do you expect "published port" reasoning to matter again later in ROOT 05+?
## Real Ops takeaway
**"Works internally, unreachable externally" is one of the single most common real-world
container networking incidents — and you just diagnosed and fixed a real one.** The exact
reasoning you used here (confirm internal health, check what's actually published, verify the
published mapping works) is the same reasoning you'll use again for Kubernetes Services later —
"published port" becomes "Service," but the underlying question (is anything actually routing
traffic to this workload?) doesn't change.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 previews Kubernetes Services directly
— worth naming that connection explicitly without teaching Services yet.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,61 @@
# Container Storage
## The default: a disposable writable layer
Every running container gets its own writable layer sitting on top of its image's read-only
layers. Anything written there — a log file, a temp file, application data — exists ONLY as long
as that specific container does. Destroy the container, and that data is gone permanently. This
is ROOT 02's "persistent vs. temporary" question, given a hard default answer: by default,
everything in a container is temporary.
## Bind mounts: sharing a specific host path
A **bind mount** connects a specific path on the HOST filesystem directly into the container:
```
docker run -v /host/path:/container/path myimage
```
Changes are visible on BOTH sides immediately — useful for development (edit code on the host,
see changes in the container instantly) but tightly coupled to that exact host's filesystem
layout, which makes it a poor choice for portable, production data.
## Named volumes: Docker-managed persistent storage
A **named volume** is storage Docker manages for you, independent of any specific host path:
```
docker volume create my-data
docker run -v my-data:/container/path myimage
```
A named volume's data survives container destruction AND recreation — attaching the SAME volume
to a brand-new container gives that new container access to exactly the same data the old one
left behind. This is the deliberate, production-appropriate way to persist container data.
## The test that actually proves persistence
Claiming "this data persists" isn't convincing without proof: write data, DESTROY the container
completely, create a brand new one attached to the same volume, and confirm the data is still
there. This module's Lab requires exactly that proof, not just a claim.
## Bridge to Kubernetes
A Kubernetes `PersistentVolume`/`PersistentVolumeClaim` is the same idea generalized across an
entire cluster instead of one machine — the exact concept this module teaches, just orchestrated
at a larger scale, which Kubernetes courses build on directly.
## Instructor Notes
**Lesson goal:** Make "containers are disposable, volumes are not" a hard, memorable
distinction — directly building on ROOT 02's persistent/temporary framing.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** assuming ANY container data survives by default — the
default is the OPPOSITE; persistence requires deliberate action (a volume or bind mount).
**Discussion question:** "Why might a named volume be preferred over a bind mount for
production data, even though both technically 'persist' data?" (Answer: a named volume isn't
tied to one specific host's exact filesystem path, making it more portable across different
hosts/environments.)
@@ -0,0 +1,63 @@
# Demonstration: Proving Data Loss, Then Proving Persistence
## OBSERVE
```
$ docker run -d --name no-volume alpine:3.19 sleep 300
$ docker exec no-volume sh -c "echo important-data > /tmp/data.txt"
$ docker exec no-volume cat /tmp/data.txt
important-data
```
Data exists — but is it actually safe?
## HYPOTHESIS
Predict: will `/tmp/data.txt` still exist if this exact container is destroyed and an identical
new one is started?
## INSPECT
```
$ docker rm -f no-volume
$ docker run -d --name no-volume alpine:3.19 sleep 300
$ docker exec no-volume cat /tmp/data.txt
cat: /tmp/data.txt: No such file or directory
```
Gone — proven, not assumed. The "new" container is a completely fresh writable layer, even
though it has the exact same name and image.
## CHANGE
Now with a named volume instead:
```
$ docker rm -f no-volume
$ docker volume create demo-vol
$ docker run -d --name with-volume -v demo-vol:/data alpine:3.19 sleep 300
$ docker exec with-volume sh -c "echo important-data > /data/data.txt"
$ docker rm -f with-volume
$ docker run -d --name with-volume -v demo-vol:/data alpine:3.19 sleep 300
```
## VERIFY
```
$ docker exec with-volume cat /data/data.txt
important-data
```
Proven, not assumed — the exact same destroy-and-recreate sequence, but this time the data
survived, because it lived in the volume, not the container's disposable writable layer.
## Instructor Notes
**Demo goal:** Prove BOTH the loss and the persistence with real destroy/recreate cycles — the
contrast is what makes this lesson land, not either half alone.
**Approximate timing:** 10 minutes live.
**Common mistake to watch for:** stopping (not removing) a container and concluding "data
persists" — `docker stop` doesn't touch the writable layer at all; only `docker rm` (destroying
the container entirely) actually tests real persistence.
@@ -0,0 +1,39 @@
# Knowledge Check: Container Storage
---
**1. By default, what happens to data written inside a running container when that container is
destroyed?**
*Answer:* It's lost permanently — a container's writable layer is disposable by default; nothing
persists unless a volume or bind mount was explicitly configured.
---
**2. What's the actual difference between a bind mount and a named volume?**
*Answer:* A bind mount connects a specific HOST filesystem path directly into the container
(tightly coupled to that host's exact layout). A named volume is storage Docker manages
independently of any specific host path, making it more portable and the production-appropriate
choice.
---
**3. Does `docker stop` (without removing) test whether a container's data actually persists?**
*Answer:* No — stopping doesn't touch the writable layer at all. Only fully removing
(`docker rm`) and recreating the container is a real test of persistence.
---
**4. What Kubernetes concept is this module's volume idea the direct foundation for?**
*Answer:* PersistentVolume/PersistentVolumeClaim — the same persistence concept, generalized
across an entire cluster instead of one machine.
## Instructor Notes
**Facilitation:** Question 3 is worth emphasizing — it's a genuine, common mistake in verifying
persistence claims.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,29 @@
# Guided Exercise: Predict What Survives Before You Test It
This is a paper exercise — commit to predictions before working in the Lab.
## Your task
For each scenario, predict whether the data survives a `docker rm -f` followed by recreating a
container with the same name and image:
1. Data written to `/tmp/file.txt` inside a container with no volumes configured.
2. Data written to `/data/file.txt`, where `/data` is mounted from a named volume.
3. Data written to `/data/file.txt` in scenario 2, but this time you ALSO run
`docker volume rm <the-volume>` before recreating the container.
## A worked shape to check yourself against
1. Lost — no volume, so it lived only in the disposable writable layer.
2. Survives — the volume itself is untouched by the container's destruction, and the new
container reattaches to the same volume.
3. Lost — the volume itself was explicitly deleted, not just the container. Persistence depends
on the VOLUME surviving, not just avoiding container destruction.
## Instructor Notes
**Facilitation:** Item 3 is the important trick question — students who've internalized
"volumes always persist" without qualification should be corrected here: the volume itself can
still be deleted deliberately.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,27 @@
# Reflection: You Proved Persistence, You Didn't Just Trust It
## Reflection questions
1. Before this module, would you have assumed container data persists by default? How
expensive would that wrong assumption be in a real production incident?
2. Why did this module insist on actually DESTROYING a container to test persistence, rather
than just describing the concept?
## Real Ops takeaway
**"I assumed it would persist" is one of the most expensive assumptions in real operations
work.** You didn't just learn that volumes persist data — you personally destroyed a container
and confirmed data loss, then destroyed another and confirmed data survival, with the SAME
sequence of commands each time except for one deliberate volume configuration. That's the level
of confidence "verify, don't assume" is supposed to give you, and it's exactly the discipline
ROOT 02's troubleshooting framework already built.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 works well as a real cautionary
example — ask if anyone has heard of (or experienced) a real data-loss incident caused by this
exact wrong assumption.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,50 @@
# Dockerfile and Image Build
## A Dockerfile is a recipe for an image
Every image you've used so far (`nginx:alpine`, `alpine:3.19`) was itself BUILT from a
Dockerfile by someone else. This module teaches you to write your own.
## The core instructions
- **`FROM <image>`** — every Dockerfile starts by choosing a base image to build on top of.
Almost nothing is built completely from scratch.
- **`WORKDIR <path>`** — sets the working directory for every instruction that follows it (and
for the container's default working directory when it runs).
- **`COPY <host-path> <image-path>`** — copies files from your BUILD CONTEXT (the directory you
run `docker build` from) into the image.
- **`RUN <command>`** — executes a command DURING the build, creating a new layer with its
result (installing a package, for example).
- **`CMD <command>`** (conceptually) — sets the DEFAULT command a container runs when started
from this image, if nothing else is specified.
- **`ENTRYPOINT <command>`** (conceptually) — similar to `CMD`, but harder to override at
`docker run` time — the two are often combined (`ENTRYPOINT` as the fixed program,
`CMD` as its default arguments).
## Build context and `.dockerignore`
`docker build -t mytag .` — that final `.` is the **build context**: the directory Docker can
see and copy files FROM during the build. A `.dockerignore` file (same idea as `.gitignore` from
ROOT 03) excludes files from the build context — keeping large or irrelevant files (like a local
`.git` folder or dependency caches) out of your image and speeding up builds.
## Layers, again
Each instruction in a Dockerfile creates one layer, cached individually. Docker reuses cached
layers for instructions that haven't changed since the last build — which is why the ORDER of
instructions matters for build speed: put things that change rarely (like `FROM` and installing
dependencies) before things that change often (like copying your actual application code), so a
small code change doesn't invalidate expensive earlier layers.
## Instructor Notes
**Lesson goal:** Give students enough Dockerfile literacy to write a real, small, working image
— not exhaustive coverage of every possible instruction.
**Approximate timing:** 18 minutes read; 10 minutes live.
**Common misconception to watch for:** copying application code before installing dependencies
— reinforce the "changes-rarely-first" layer-ordering principle with a concrete example.
**Demo idea:** build the same Dockerfile twice with no changes, showing the second build using
fully cached layers (`CACHED` in the output) and finishing almost instantly.
@@ -0,0 +1,62 @@
# Demonstration: Watching Layer Caching Actually Work
## OBSERVE
```
$ docker build -t demo:1 .
Step 1/3 : FROM alpine:3.19
Step 2/3 : WORKDIR /app
Step 3/3 : COPY greeting.txt .
Successfully tagged demo:1
```
## HYPOTHESIS
Predict: if you run the EXACT same build again with no changes, will it take the same amount of
time?
## INSPECT
```
$ docker build -t demo:1 .
Step 1/3 : FROM alpine:3.19
Step 2/3 : WORKDIR /app
---> Using cache
Step 3/3 : COPY greeting.txt .
---> Using cache
Successfully tagged demo:1
```
`Using cache` — nearly instant, because nothing actually changed since the last build.
## CHANGE
Now change `greeting.txt`'s content and rebuild:
```
$ echo "updated" > greeting.txt
$ docker build -t demo:1 .
Step 2/3 : WORKDIR /app
---> Using cache
Step 3/3 : COPY greeting.txt .
---> (this step re-runs — the file changed)
```
Only the step AFTER the change re-runs — `WORKDIR` (unaffected) still used its cache.
## VERIFY
This is exactly why instruction ORDER matters: everything before the first changed instruction
stays cached; everything from that point onward must rebuild. Put your least-frequently-changing
instructions first.
## Instructor Notes
**Demo goal:** Make layer caching a directly OBSERVED behavior (via real build output), not an
abstract efficiency claim.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** ordering a Dockerfile with frequently-changing application code
copied BEFORE installing rarely-changing dependencies — invalidates the expensive dependency-
install cache on every trivial code change.
@@ -0,0 +1,38 @@
# Knowledge Check: Dockerfile and Image Build
---
**1. What does `WORKDIR` actually set?**
*Answer:* The working directory for every subsequent instruction in the build, and the
container's default working directory when it runs.
---
**2. What is the "build context"?**
*Answer:* The directory (usually specified as `.` at the end of `docker build -t tag .`) that
Docker can see and copy files FROM during the build — `COPY` instructions read from it.
---
**3. Why does instruction order in a Dockerfile affect build speed?**
*Answer:* Docker caches each instruction's result as a layer and reuses cached layers for
unchanged instructions — everything before the first ACTUALLY-changed instruction stays cached,
so putting rarely-changing instructions (like dependency installs) before frequently-changing
ones (like application code) minimizes unnecessary rebuilding.
---
**4. What's `.dockerignore` for, and what other file does it resemble?**
*Answer:* It excludes files from the build context (keeping large/irrelevant files out of the
image and speeding up builds) — the same idea as ROOT 03's `.gitignore`.
## Instructor Notes
**Facilitation:** Question 3 directly reinforces the caching demo — a good check that the
concept, not just the observation, landed.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,35 @@
# Guided Exercise: Order These Instructions for Maximum Cache Efficiency
This is a paper exercise — commit to an order before checking it in the Lab.
## Your task
You need a Dockerfile with these five things, in SOME order. Application code changes very
often; dependencies change rarely. Write the most cache-efficient order:
- `FROM python:3.11-slim`
- `COPY app.py .` (your actual application code — changes often)
- `COPY requirements.txt .` (dependency list — changes rarely)
- `RUN pip install -r requirements.txt` (installs dependencies — slow)
- `WORKDIR /app`
## A worked shape to check yourself against
```
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app.py .
```
Copying `requirements.txt` and installing dependencies BEFORE copying the actual application
code means a code-only change never invalidates the (slow) dependency-install layer — only the
final `COPY app.py .` step needs to rerun.
## Instructor Notes
**Facilitation:** This is one of the most practically useful habits in the entire module — worth
connecting explicitly to real build-time savings on larger, slower dependency installs.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,24 @@
# Reflection: You Built a Real Image, Not Just Ran Someone Else's
## Reflection questions
1. Every image you used before this module (nginx, alpine) was built by someone else, from a
Dockerfile you never saw. Does writing your own change how you think about those images?
2. Which Dockerfile instruction do you expect to use the most in real application work?
## Real Ops takeaway
**A Dockerfile is executable documentation of exactly how an application's environment is
built — the same reproducibility promise ROOT 03's version control made about code, now applied
to environments.** You've closed the loop this course opened with Module 1's "it works on my
machine" problem: a Dockerfile means the environment itself is defined, versioned, and
rebuildable identically anywhere, by anyone, forever.
This module is complete. Continue to the next module.
## Instructor Notes
**Discussion questions for a live classroom:** question 1 is a good moment to reflect on the
"black box → understood mechanism" arc of this entire course.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,65 @@
# Docker Compose
## Real applications are rarely just one container
A web app usually needs a database, maybe a cache, maybe a background worker — several
containers that need to be created, networked together, and started in a sensible order. Doing
this manually with individual `docker run` commands works, but gets tedious and error-prone fast,
especially remembering the exact flags every time.
## One file, the whole application
**Docker Compose** describes an entire multi-container application in one YAML file:
```yaml
services:
web:
image: nginx:alpine
ports:
- "8080:80"
depends_on:
- api
api:
image: my-api:1
```
- **`services`** — each entry is one container, using the exact same concepts (image, ports,
etc.) you already know from plain `docker run`.
- **`depends_on`** — controls STARTUP order (api starts before web) — note this ensures order,
not that the dependency is fully READY yet, a subtlety real applications often need extra
handling for.
- **Networking is automatic** — Compose creates a shared network for all services in the file by
default, so `web` can reach `api` by name immediately, no manual `docker network create`
needed.
## The commands
```
docker compose up -d # start every service defined in the file, in the background
docker compose ps # see the status of every service
docker compose logs # see logs from every service, or docker compose logs <service>
docker compose down # stop and remove everything Compose created
```
## Compose doesn't replace anything you've learned — it automates it
Every Compose service maps directly onto a `docker run` command you could write by hand; Compose
just describes the whole set declaratively in one file instead of remembering and re-typing
several long commands correctly, every time, in the right order.
## Instructor Notes
**Lesson goal:** Frame Compose as automation of already-understood concepts, not new magic —
students should be able to mentally translate a `services:` block back into individual
`docker run` commands.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating `depends_on` as "waits until fully healthy" —
it only controls START order, not readiness; worth flagging as a real, common source of race
conditions in actual production Compose files.
**Discussion question:** "Why might describing your application declaratively in one file be
better than a shell script that runs several `docker run` commands in sequence?" (Good answer:
reviewable, versionable, and Compose itself understands the relationships between services,
rather than just blindly executing a sequence.)
@@ -0,0 +1,64 @@
# Demonstration: One File, Two Containers, One Network — Automatically
## OBSERVE
```yaml
services:
web:
image: nginx:alpine
ports:
- "8080:80"
cache:
image: redis:alpine
```
## HYPOTHESIS
Predict: without running `docker network create` yourself, will `web` be able to reach `cache`
by name after `docker compose up`?
## INSPECT
```
$ docker compose up -d
$ docker compose exec web sh -c "wget -qO- http://cache:6379/ 2>&1 | head -1"
```
(Redis doesn't speak HTTP, so this specific request looks odd, but the connection ITSELF
succeeding or failing tells you whether name resolution and network reachability work — a
`Connection refused` here would mean it REACHED `cache` but Redis rejected the protocol
mismatch, which is actually proof the network worked.)
## CHANGE
```
$ docker compose ps
NAME IMAGE STATUS
web-1 nginx:alpine Up
cache-1 redis:alpine Up
$ docker network ls | grep compose
xyz123 myproject_default bridge
```
Compose created a network automatically, named after the project directory, with no manual step.
## VERIFY
```
$ docker compose down
$ docker network ls | grep compose
(nothing)
```
`docker compose down` cleaned up EVERYTHING Compose created — containers AND the network — in
one command.
## Instructor Notes
**Demo goal:** Show the automatic networking as a real, observable fact (a real network exists,
named and scoped to the project) rather than an assumed convenience.
**Approximate timing:** 8 minutes live.
**Common mistake to watch for:** forgetting `docker compose down` and accumulating orphaned
networks/containers across multiple exercises — a good habit to reinforce explicitly.
@@ -0,0 +1,40 @@
# Knowledge Check: Docker Compose
---
**1. What does `depends_on` actually guarantee — and NOT guarantee?**
*Answer:* It guarantees START order (the dependency's container is started before the dependent
service's). It does NOT guarantee the dependency is actually READY/healthy yet — a real,
common source of race conditions in production Compose files.
---
**2. Do you need to manually run `docker network create` for services in the same Compose file
to reach each other by name?**
*Answer:* No — Compose automatically creates a shared network for all services defined in the
file, and they can reach each other by service name immediately.
---
**3. What does `docker compose down` clean up, that plain `docker compose stop` would not?**
*Answer:* `down` removes the containers AND the network Compose created; `stop` only stops the
containers, leaving them (and the network) in place to be restarted later.
---
**4. Does Compose introduce any concept that isn't already just automating `docker run`/
`docker network` commands you already know?**
*Answer:* No — every Compose service maps directly onto concepts already covered
(image, ports, networking); Compose automates and declaratively describes them, it doesn't add
new underlying mechanisms.
## Instructor Notes
**Facilitation:** Question 1 is worth real emphasis — it's a genuine, common production
mistake worth preventing early.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,36 @@
# Guided Exercise: Translate a Compose File Back Into Plain `docker run`
This is a paper exercise — commit to your translation before checking it in the Lab.
## Your task
Given this Compose service definition, write the equivalent plain `docker run` command by hand:
```yaml
services:
web:
image: nginx:alpine
ports:
- "8080:80"
```
Then explain: what would you ALSO need to do manually (that Compose does automatically) if two
services needed to reach each other by name?
## A worked shape to check yourself against
```
docker run -d -p 8080:80 nginx:alpine
```
For two services to reach each other by name manually, you'd need to `docker network create` a
shared network yourself, then add `--network <that-network>` to EACH container's `docker run`
command — exactly the manual step Compose eliminates.
## Instructor Notes
**Facilitation:** This exercise is the clearest possible proof that Compose isn't new magic —
being able to translate it back to `docker run` confirms genuine understanding, not just
familiarity with YAML syntax.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced.
@@ -0,0 +1,26 @@
# Reflection: A Real Multi-Service Application, Described and Running
## Reflection questions
1. Before this module, how would you have run two related containers and connected them
manually? How does that compare to this module's Lab?
2. What does a Compose file's `services:` block remind you of, from ROOT_'s later Kubernetes
content — even without knowing Kubernetes syntax yet?
## Real Ops takeaway
**You just described and ran a real multi-container application declaratively — the exact same
"describe what you want, let the tool make it real" pattern behind every later Kubernetes manifest
in this program.** This isn't a coincidence: Compose and Kubernetes both solve "orchestrate
multiple containers as one application," at different scales. The habit of thinking in terms of
declared services rather than manual step-by-step commands is what ROOT 05 builds on directly.
This module is complete. Continue to the final module of this course.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 previews Kubernetes manifests
directly — a good moment to name "declarative description of desired state" as a term students
will hear constantly starting in ROOT 05.
**Approximate timing:** 5-8 minutes live discussion; 5 minutes self-paced writing.
@@ -0,0 +1,64 @@
# Containers → Kubernetes
## What you've actually built, across this entire course
You can now package an application (Dockerfile), run it in isolation (containers), connect
multiple containers together (networking, Compose), and persist their data reliably (volumes).
That's a complete, real skill set for running an application on ONE machine. Kubernetes exists
to answer the question this course deliberately never had to ask: **what happens when "one
machine" isn't enough?**
## What Docker already solves
- **Application packaging** — a Dockerfile makes an environment reproducible anywhere.
- **Process isolation** — a container is isolated from other processes on the same machine.
- **Basic networking** — containers can reach each other and be reached from outside, on one
host.
- **Basic multi-container orchestration** — Compose runs a small set of related containers
together, on one host.
## What Kubernetes adds on top
- **Scheduling** — deciding WHICH machine, out of potentially hundreds, a container should run
on — a question that doesn't exist when you only have one machine.
- **Desired state and reconciliation** — you declare what SHOULD be running; Kubernetes
continuously works to make reality match that declaration, even after a machine fails.
- **Controllers** — automated processes that watch and correct drift from desired state (the
generalized version of "restart it if it crashes," running constantly, across the whole
cluster).
- **Service discovery at scale** — reaching "the right instance of a service" when there might
be dozens of them, spread across many machines, changing constantly.
- **Health management** — automatically detecting an unhealthy container and replacing it,
without a human first noticing something's wrong.
- **Scaling** — running more or fewer copies of something based on real demand, automatically.
- **Rollout strategies** — deploying a new version across many running instances safely, with a
defined strategy for how to do it without an outage.
- **Distributed orchestration** — coordinating ALL of the above across a fleet of machines as if
it were one system.
## The honest bridge
None of this makes Docker/containers obsolete — Kubernetes runs containers, using the exact
container runtime concepts this course just taught. Kubernetes is the layer ABOVE containers
that exists specifically because real production systems eventually need more than one machine,
more reliability than manual restarts, and more automation than a human watching dashboards. You
are not about to learn a replacement for what you just built — you're about to learn what
happens when you need to run it at a scale one machine can't handle.
## Instructor Notes
**Lesson goal:** Deliver the explicit "why Kubernetes" answer this entire foundational half of
ROOT_ has been building toward — students should be able to state, in their own words, what
specifically Kubernetes adds that Docker alone doesn't.
**Approximate timing:** 15 minutes read; 10 minutes live.
**Common misconception to watch for:** treating Kubernetes as "a replacement for Docker" rather
than "an orchestration layer that runs containers across many machines" — the container
concepts from this entire course remain fully applicable inside Kubernetes.
**Discussion question:** "Name one thing from this list (scheduling, desired state, controllers,
service discovery, health management, scaling, rollout, distributed orchestration) that would be
genuinely hard to do by hand with just `docker run` commands across many machines." (Almost any
honest answer works — the point is feeling the real gap Kubernetes fills, not memorizing the
list.)
@@ -0,0 +1,40 @@
# Knowledge Check: Containers → Kubernetes
---
**1. Does Kubernetes replace Docker/containers?**
*Answer:* No — Kubernetes orchestrates containers across many machines; it runs on top of the
same container concepts this course taught, not instead of them.
---
**2. Name one thing Kubernetes adds that a single-machine `docker run`/Compose setup
fundamentally can't do.**
*Answer:* Any reasonable answer from: scheduling across multiple machines, automatic
reconciliation to a desired state after a failure, service discovery across many changing
instances, automated health-based replacement, demand-based scaling, or coordinated rollout
across a fleet.
---
**3. What does "desired state" mean, in the sense this lesson introduced it?**
*Answer:* You declare what SHOULD be running; the orchestrator continuously works to make actual
reality match that declaration, including automatically after a failure — rather than a person
having to manually notice and fix drift.
---
**4. Why doesn't this course teach Kubernetes directly, ending here instead?**
*Answer:* This course's job was building the container/Linux/Git foundation Kubernetes assumes
— ROOT 05 builds on exactly this foundation to teach Kubernetes itself.
## Instructor Notes
**Facilitation:** Question 2 is worth collecting several different answers from the group — the
variety itself is evidence the "why Kubernetes" question actually landed.
**Approximate timing:** 5 minutes.
@@ -0,0 +1,33 @@
# Reflection: You're Ready, By Design, Not By Accident
## Reflection questions
1. Look back across ROOT 01 (Linux/processes), ROOT 02 (networking/troubleshooting), ROOT 03
(Git), and ROOT 04 (containers). Which single skill do you expect to use MOST once you start
ROOT 05?
2. Can you explain, in your own words and without looking anything up, why Kubernetes exists?
If you can't yet, which part of this module's lesson would you reread?
## Real Ops takeaway
**Every module across all four foundational courses was chosen deliberately to make THIS moment
make sense.** Processes (ROOT 01) become container processes become Pods. Filesystem/storage
(ROOT 01-02) becomes container volumes becomes Kubernetes storage. Networking/DNS/troubleshooting
(ROOT 02) becomes container networking becomes Services/Ingress/NetworkPolicy. Git (ROOT 03)
becomes the foundation for GitOps later. None of ROOT 05's content will be starting from zero —
you are walking in already equipped, on purpose.
This module — and the entire ROOT_ Foundations sequence (ROOT 01-04) — is complete. Continue to
ROOT 05: Kubernetes Foundations.
## Instructor Notes
**Discussion questions for a live classroom:** question 2 is the single best readiness check for
this entire foundational sequence — a confident, correct answer here is real evidence students
are ready for ROOT 05, not just that they clicked through four courses.
**Approximate timing:** 8-10 minutes live discussion; 5 minutes self-paced writing.
**Milestone note:** this is the last module of the entire ROOT_ Foundations sequence (ROOT 01-04)
— consider a genuine celebratory moment here for a live cohort; this represents a real, complete
skill foundation, not just a checkpoint.
@@ -0,0 +1,39 @@
# Practice Bank: Deployments & Workloads
This module is different from the ones before it. There's no new lesson here — you already got
the real teaching in **Deployments, Scaling & Updates** and **Jobs, CronJobs & DaemonSets**. This
is eight short, focused Labs that drill the exact same skills at speed: deploy, scale, roll
forward, add a liveness probe, edit a live object, and reason about Jobs/CronJobs/DaemonSets
without a guided walkthrough alongside you.
## Why this exists
Real fluency with `kubectl` comes from repetition, not from reading about it a second time. Each
Lab here is a small, self-contained task — no long setup, no new concepts — so you can move
quickly and build the kind of muscle memory that matters when you're troubleshooting a real
cluster under time pressure later in this program.
## How to use it
Work through the eight Labs in order. If one takes you more than a few minutes, that's useful
information — it usually means the earlier module's concept hasn't fully clicked yet, and it's
worth a quick re-read of that lesson before continuing rather than pushing through by trial and
error.
## Instructor Notes
**Delivery:** This module has no lesson/demonstration/knowledge-check scaffolding by design — it
is the original Phase 1-5 "workloads" scenario bank, reused here as a self-paced speed-practice
set once the real teaching (root-06-m02/m03) has already landed. In an instructor-led cohort,
this is a good candidate for a timed "workloads sprint" between the teaching modules and Jobs.
**Capstone note:** These eight scenarios are also the exact set the pre-existing
`rootnode-path.json` "workloads" stage requires to unlock the Capstone assessment (a separate,
older mechanism from this course's own prerequisite chain — see
docs/ROOT_V1_CURRICULUM_MANIFEST.md). Do not mark this module optional/is_required=false without
re-checking that gate; a student can only be told to skip it if the Capstone gate itself is
intentionally changed too.
**Common mistake to watch for:** students treating the MCQ-style activities
(jobs-cronjobs-mcq, daemonset-mcq, deployment-strategy-mcq) as throwaway trivia — they're
checking the same conceptual judgment calls the reflection in root-06-m03 raised, just faster.
@@ -0,0 +1,29 @@
# Practice Bank: Services & Networking
Like the workloads practice bank before it, this module has no new lesson. **Services & Service
Discovery** and **Ingress & NetworkPolicy Foundations** already taught the concepts. This is four
short Labs that drill exposing a Deployment, reasoning about a NodePort, and resolving a Service
by DNS from inside the cluster — the everyday networking moves you'll use constantly from here on.
## How to use it
Work through the four Labs in order. Two of them (`services-mcq`, `expose-service-basics`) will
feel familiar — you did a close variant of `expose-service-basics` in the earlier teaching
module. That's intentional: it's the same skill under slightly different conditions, checking
that it generalizes rather than that you memorized one exact command.
## Instructor Notes
**Delivery:** No lesson/demonstration/knowledge-check scaffolding by design — this is the
original Phase 1-5 "networking" scenario bank plus two extra Labs (`nodeport-task`,
`dns-resolution-mcq`) not part of that original set, reused here as self-paced speed practice.
**Capstone note:** `services-mcq` and `expose-service-basics` are the exact pair the
pre-existing `rootnode-path.json` "networking" stage requires to unlock the Capstone assessment
— a separate, older mechanism from this course's own prerequisite chain (see
docs/ROOT_V1_CURRICULUM_MANIFEST.md). Do not mark this module optional/is_required=false without
re-checking that gate.
**Common mistake to watch for:** confusing a NodePort's cluster-internal ClusterIP with the
node-level port it also opens — the DNS-resolution Lab is a good moment to have a student narrate
out loud which address they expect to work from where.
@@ -0,0 +1,30 @@
# Practice Bank: General Troubleshooting Operations
**Kubernetes Health Probes & Troubleshooting** and **The RootNode Troubleshooting Framework**
already taught you how to *think* about a broken cluster. This module is six short Labs that
drill the individual `kubectl` operations you'll reach for while doing that thinking: pulling
logs, copying files in and out of a container, running a one-off exec, cordoning a node, and
reasoning about pod-to-node placement and log output.
## How to use it
Work through the six Labs in order. None of them require the full Observe/Evidence/Hypothesis/
Test/Repair/Validate/Prevent framework from the previous module — they're deliberately narrower,
so you can build raw command fluency without also holding the whole framework in your head at
the same time.
## Instructor Notes
**Delivery:** No lesson/demonstration/knowledge-check scaffolding by design — this is the
original Phase 1-5 "troubleshooting" scenario bank plus one extra Lab (`container-logging-mcq`)
not part of that original set, reused here as self-paced speed practice.
**Capstone note:** The five original Labs (`extract-logs-basics`, `kubectl-cp-basics`,
`exec-command-basics`, `node-cordon-basics`, `pod-node-assignment-mcq`) are the exact set the
pre-existing `rootnode-path.json` "troubleshooting" stage requires to unlock the Capstone
assessment — a separate, older mechanism from this course's own prerequisite chain (see
docs/ROOT_V1_CURRICULUM_MANIFEST.md). Do not mark this module optional/is_required=false without
re-checking that gate.
**Common mistake to watch for:** running `kubectl cp` with the source/destination arguments
reversed — a good moment to have a student read the command back out loud before running it.

Some files were not shown because too many files have changed in this diff Show More