Teaching
container-examples
Four hello-world web apps in Go, Flask, Django and React, each with a Containerfile that packages it into an image.
I wrote these over three days in June 2023. Each directory holds a small app and the Containerfile that builds it.
golang is a Fiber server that answers on port 3000. Its build has two stages: the first, on golang:1.20-alpine, runs go mod tidy and go build, and the second copies in only the compiled binary.
python-flask and python-django use the same Containerfile and the same nginx config, and both apps return a JSON greeting at /hello. On top of python:3.9 the image installs nginx, adds an unprivileged user called customuser, gives it /app and nginx’s log and state directories, and switches to it. uWSGI runs up to six worker processes behind a Unix socket, /app/app.sock. nginx listens on 8080 and hands requests to that socket with uwsgi_pass. The start command puts uWSGI in the background and keeps nginx in the foreground, so one container runs both.
javascript-react is the default Create React App page. A node:14 stage runs npm run build, and the output is copied into nginx:alpine, which serves it on port 80. Any path that is not a file gets index.html, so client-side routes still load the app.
The Go runtime stage reuses golang:1.20-alpine, compiler included, so it should not be copied as it is; the README lists that and the other limits of the examples. The Django example reads its secret key, debug flag and allowed hosts from the environment.
container-tshoot
Small container builds and compose files that fail on purpose, for practising container troubleshooting with Podman.
I made these in November 2025. Each directory holds an image or a compose project that builds but does not run correctly, and the learner has to find the fault and fix it. The README gives the commands for each scenario, the error it should produce and one hint towards the cause, with the Docker equivalents where they differ. SOLUTIONS.md has the fixes, so a learner can work through a scenario alone and check afterwards instead of asking me.
broken-app copies app.py into the image as /app/ap.py, while the start script runs /app/app.py. The container prints “Starting app” and then a Python error about a missing file. Correcting the file name is the fix, and the working container prints a greeting and exits.
webapp-port is a Python web server with two faults stacked on each other. It reads PORT from the environment as a string and hands it straight to the socket, which fails with a TypeError. Once the port is an integer, the server still binds to 127.0.0.1 inside the container, so a published port reaches nothing until it listens on 0.0.0.0.
multicontainer-app is a Flask app and MySQL 8 in one compose file. The app is told the database is at localhost, but MySQL runs in its own container; the learner points DB_HOST at the service name, db.
podman-compose.yml at the top of the repository runs WordPress with MySQL and is not one of the exercises. Nothing in it is broken. It is there as a working two-container setup to compare with multicontainer-app: the application reaches its database by service name over a shared bridge network, which is what the broken one gets wrong.
linux-labs
Fifty-five self-grading Linux administration labs for Rocky Linux and RHEL 8 and 9, shipped as a signed RPM from a dnf repository and run through a small command-line tool.
I wrote these labs so that a learner practises on real machines and finds out at once whether the result is right. The setup is the Red Hat classroom one: a workstation where the learner runs labctl, and servers named servera, serverb and serverc where the work happens. The learner logs in to servera as opsadmin with full sudo, while on the workstation they may only run labctl.
There are 55 labs in 17 topics, from beginner to advanced: files, users, packages, networking, firewalld, logging, scheduling, SELinux, storage, systemd, Apache, MySQL, PostgreSQL, BIND, database replication, load balancing and Pacemaker clustering. In replication-03 the learner builds circular MySQL replication across three nodes; clustering-03 protects a three-node cluster against split-brain with quorum settings.
labctl start copies the lab to servera, prepares it there and prints the task, which states the end state and never the commands. labctl hint reveals one hint at a time, labctl grade checks only the final state of the machines and prints a pass or fail line per criterion, so any correct sequence of commands passes, and labctl reset puts the machines back. labctl check tells a teacher before the first lesson whether the keys, sudo and servers are set up.
Reset is strict. Every package, repository key and system account a lab added since it started is removed again, and what the server had before stays, so labs cannot leak into each other. A test script plays every lab from start to reset on real virtual machines and fails when anything is left behind. Before version 2.0 I ran all 55 labs on Rocky Linux 8.10 and 9.8 that way.
The package is built and signed in GitHub Actions and published to a dnf repository on GitHub Pages, so a classroom installs it with one command:
curl -fsSL https://linux-labs.matejbasic.com/install | sudo bash
The same pages carry the lab catalog and the student guides.
Multi-node labs
The load balancing, replication and clustering labs use all three servers. labctl configure stores the node addresses and the SSH user and key, and the lab scripts source a Bash library with get_node_ip, run_on_node and wait_for_node. That is how the grader for replication-01 checks binary logging on one node and the replica threads on the other, over SSH.
- Code: github.com/matej-basic/linux-labs
- Live demo: linux-labs.matejbasic.com