Your next deployment starts here

Build the habit
behind the ship.

A practical control room for learning how technical work moves from a checked change to a reliable result.

17%path complete
01active lab
15course materials
runbook / delivery-loopLIVE
01 assignment.acceptdone
02 repo.clone privateready
03 git.push mainpassed
04 actions.gradewaiting
05
01 / PATH

From first check
to calm handoff

Four chapters. One observable habit.

✓
Orientation1/2 lessons
2
Delivery loop0/3 lessons
3
Network lab0/2 lessons
4
Operations0/2 lessons
02 / COURSE LIBRARY

Lectures and practices, in one place

The authenticated Maru items are paired with the original slides and practice briefs, so every task has a traceable source.

Lecture 01W01 · 60 min lecture

Welcome & the DevOps landscape

"But it works on my machine."

Topics
"But it works on my machine."What is DevOps, really?The wall between Dev and OpsDevOps is a culture, not a tool — CALMS
Lecture 02W02 · 60 min lecture

Git/GitHub workflow & Linux CLI refresher

Git fundamentals

Topics
Git fundamentalsGit tracks snapshots, not filesThe three statesA commit, edited live
Lecture 03W03 · 60 min lecture

Containers 101: Why & How

Lecture 3: Containers 101 — Why & How

Topics
Lecture 3: Containers 101 — Why & HowProcesses, VMs, and ContainersThree ways to isolate an appThe key insight
Lecture 04W04 · 60 min lecture

Images, Layers & Multi-Stage Builds

Lecture 4: Images, Layers & Multi-Stage Builds

Topics
Lecture 4: Images, Layers & Multi-Stage BuildsWhat's actually inside an image?An image is layers, not a blobEvery instruction is (usually) a layer
Lecture 05W05 · 60 min lecture

Container Networking & Volumes

Lecture 5: Container Networking & Volumes

Topics
Lecture 5: Container Networking & VolumesNetworking fundamentalsIsolation by defaultPublishing a port
Lecture 06W06 · 60 min lecture

Container Security & Module Wrap-up

Lecture 6: Container Security & Module Wrap-up

Topics
Lecture 6: Container Security & Module Wrap-upContainers are not VMsWhat actually isolates a containerRoot in the container = root on the host?
Lecture 07W07 · 60 min lecture

Configuration Management & Ansible Basics

Lecture 7: Configuration Management & Ansible Basics

Topics
Lecture 7: Configuration Management & Ansible BasicsWhy configuration managementDrift and snowflakesImperative vs declarative
Lecture 08W08 · 60 min lecture

Playbooks, Roles & Variables

Lecture 8: Playbooks, Roles & Variables

Topics
Lecture 8: Playbooks, Roles & VariablesPlaybooks in depthA playbook is a list of playsbecome: who runs the task
Practice 02W02 · ~25 min of actual work within today's session.

Practice 02 — Your first commits

make real commits that tell a story — the add/commit/push loop from today's lecture, for real, on your own repo.

Definition of done
✓ At least 3 commits in your PR✓ Both placeholder lines in NOTES.md are replaced with real content✓ CONTRIBUTORS.md exists and contains your name

NOTES.md (in this same folder) has two placeholder lines. Do the following, as three separate commits (not one big commit at the end): 1. Edit NOTES.md — replace the "What I learned about Git today" placeholder with a real sentence. Commit it with a message describing what you learned, not just "update notes". 2. Edit NOTES.md again — replace the "A command I want to remember" placeholder with an actual Git command from today plus a one-line note on when you'd use it. Commit it separately from step 1. 3. Create a n

Practice 03W03 · ~30 min of actual work within today's session.

Practice 03 — Write a Containerfile

write a Containerfile from scratch for a small app — base image, dependency install, non-root user, in the right order — the skill from today's lecture.

Definition of done
✓ Containerfile exists at the repo root✓ It builds successfully✓ The container runs and responds on port 8080

Your repo has one folder: app/ — a tiny Flask app (app.py) and its requirements.txt. You did not write this app and don't need to touch it. Your job is to containerize it. Add a file named exactly Containerfile at the root of the repo (not inside app/) that:

Practice 04W04 · ~30 min of actual work within today's session.

Practice 04 — Multi-Stage Builds

write a multi-stage Containerfile that separates build-time tooling from the runtime image — the skill from today's lecture — and ship a final image under 50 MB.

Definition of done
✓ Containerfile exists at the repo root✓ It has a multi-stage structure: 2+ FROM instructions, a named build stage, and a COPY --from=✓ It builds successfully

Your repo has one folder: app/ — a tiny Go HTTP server (main.go) and its go.mod, responding with exactly INF345 Practice 04 OK on port 8080. You did not write this app and don't need to touch it. Your job is to containerize it properly — with a multi-stage build, not a single-stage one. Add a file named exactly Containerfile at the root of the repo (not inside app/) that:

Practice 05W05 · ~30 min of actual work within today's session.

Practice 05 — Container Networking & Volumes

wire two containers together with Compose — a user-defined network so they can reach each other by service name, and a named volume so data survives a restart — the skill from today's lecture.

Definition of done
✓ compose.yaml exists at the repo root and docker compose config validates it✓ docker compose up -d --build brings up both containers and GET /healthz responds on :8080✓ Two requests to GET / show the counter increasing

Your repo has two things already done, which you do not need to touch: - app/ — a tiny Go HTTP server that serves a visit counter it keeps in Redis (GET /, reading REDISADDR, default redis:6379) and a liveness probe (GET /healthz). - app/Containerfile — already builds and runs correctly (last week's skill).

Practice 06W06 · ~30 min of actual work within today's session.

Practice 06 — Container Security

take a Containerfile that works and make it safe to ship: non-root, pinned, minimal, no secrets inside, and still working when the runtime takes almost every privilege away — the skills from today's lecture.

Definition of done
✓ Every FROM is pinned to a tag (not latest) or a digest✓ Multi-stage build; final image under 50 MB✓ The final image has a non-root USER; GET / shows uid= not 0

Your repo has: - main.go — a tiny Go HTTP server. Don't touch it. GET / answers INF345 Practice 06 OK uid=<uid> token=<set|missing> — it reports which user it runs as, and whether APITOKEN was given to it (never the value). On startup it writes a small file to /tmp. - .env — a local-development secret (APITOKEN=...). It's a fake token, but treat it exactly like a real one. - Containerfile — builds and runs fine. It is also a security review's worst nightmare. Every line marked PROBLEM is something to fix.

Practice 07W07 · ~30 min of actual work within today's session.

Practice 07 — Your First Playbook

describe a small "deployment" as Ansible code — an inventory, one play, four real modules, variables — and prove it's idempotent: the second run changes nothing. The skills from today's lecture.

Definition of done
✓ ansible-inventory --graph shows localhost in @web, with a local connection✓ ansible-playbook site.yml --syntax-check passes✓ The playbook runs and creates the directory (0755), index.html (0644, identical to files/index.html), app.conf and .env

Your repo has these already done, which you do not need to touch: - ansible.cfg — points Ansible at inventory.ini. - files/index.html — a page to deploy. - templates/app.conf.j2 — a Jinja2 template for the app's config, using appname, appport and appenv.

Practice 08W08 · ~30 min of actual work within today's session.

Practice 08 — Refactor into a Role

take last week's playbook and turn it into a reusable role — variables as role defaults, tasks in the role, a handler that runs only when something changed — and see variable precedence at work: groupvars beat role defaults, -e beats everything. The skills from today's lecture.

Definition of done
✓ roles/webapp/ has tasks/, defaults/, handlers/, templates/, files/✓ The defaults are exactly the four variables above✓ site.yml only applies the role; --syntax-check passes

Do the TODOs in order: 1. site.yml — one play on web that applies the role: roles: [webapp]. No tasks in site.yml itself. 2. roles/webapp/defaults/main.yml — approot: /tmp/inf345-p08/app, appname: inf345, appport: 8080, appenv: production. Defaults are the weakest variables — that's what lets users of the role override them. 3. – 6. roles/webapp/tasks/main.yml — last week's four tasks, now under {{ approot }}: the directory (file, 0755), index.html (copy, 0644), app.conf (template) and the APPENV line in .env (line

02 / CONTINUE

Pick up the thread

02 / 06
Orientation12 minSource linked

Read your delivery board

Turn To do, Coming up, and Earlier into a simple operating rhythm for technical work.

JR3 learners here
17%complete
03 / PRACTICE LABS

Work in a safe loop

01
Mock repository10 min

Repair the handoff

A safe sandbox for a failing push and a missing invite.

◉Ready
02
Packet Tracer22 min

Read the network

Inspect a starter topology and submit the checked artifact.

◌Locked
03
Practice14 min

Close the loop

Answer five source-backed checks and keep your first score separate.

◌Locked
06 / COURSE NOTES

The course now has a traceable spine.

Each lecture and practice links back to its Maru item and the original INF 345 repository. The workspace keeps the imported brief visible while leaving the canonical source one click away.

8lectures linked
7practices linked
0runtime AI calls