Read the lesson. Then prove it in a real terminal beside it.
An in-house cybersecurity LMS for a school's own students: every lesson has a real, disposable Linux environment at its side. Read the concept, then actually use it — locked, verified, and made airtight to run safely on one shared production box.
$ ./lmsctl start --exercise linux-basics-1 → publish: 127.0.0.1:5000/linux-basics-1 → creating disposable container ...... ok → network mode: none → limits: 256MiB · 1 CPU · 64 PIDs → websocket bridge ................... connected $ find /opt/exercise -name "flag.txt" /opt/exercise/.hidden/flag.txt $ cat /opt/exercise/.hidden/flag.txt CYCLONE{read_then_prove} ✓ flag verified · progress recorded · cleanup complete
One learning loop,
two connected systems.
The read side and the practice side sit beside each other, joined by a live pipe. Log in once, read the concept, then flip to a real shell and prove it — the platform keeps the two connected without sharing state between independent exercises.
A real shell, with deliberate boundaries.
The Go server creates a fresh Docker container, attaches a browser terminal through WebSockets, then stops and removes the environment when the session ends.
Cybersecurity concepts
are not enough.
Public platforms like HTB and TryHackMe prove the format — but a school handing real students shells needs it in-house: its own curriculum, its own progress data, no per-seat licensing. And generally, reading about commands is different from using them; learners need a safe way to experiment with real shell environments without access to the host machine, session interference, or abandoned containers.
Passive learning
does not create operational confidence.
Shared environments
create state leakage between learners.
Unbounded containers
can exhaust host CPU and memory.
From lesson page to
isolated practice session.
Click a node to inspect its responsibility. Login sits in front of everything; every layer after it is a deliberate boundary — least privilege, ephemeral lifecycles, resource bounds — so a shell-spawning app can run beside four other production apps on one host.
Go lms-server
Owns routing, validation, sessions, and container lifecycle orchestration.
Every measure exists
because we thought about it.
Nothing here is best-effort. Each control answers one question — what happens on a public box if this goes wrong? A shell-spawning app earns the right to sit beside four other production apps through least privilege, not good intentions.
Privileged: falseblocked by defaultMemory: 256 MiB · NanoCPUs: 1 · PidsLimit: 64boundedIdle timeout · stop · remove · shutdown cleanupownedProduction came for it —
and the design held.
The nightly Docker Cleanup on the shared box deletes any image nothing currently references. Practice containers are disposable by design — so the exercise images were unanchored, and sessions briefly hit No such image. Because images were already published to the local registry as permanent spares, the daemon pulls them back on demand: a pruned copy costs one extra pull, never a broken session.
Reading becomes
operational confidence.
Implementation outcomes, not invented metrics. A school's students read a lesson and immediately prove it in a real shell — safely, on the box the school already pays for.
Follow one
practice session.
Move through the lifecycle to see what the application is doing at each boundary. The terminal above is a clearly labeled simulation of the flow.
Server rate-limits and validates submission.
constant-time compare · accepted→ event acknowledged · next boundary ready
How a lesson becomes a disposable Linux lab in the browser.
Architecture walkthrough by Cynthia Wahome
Watch walkthroughSmall system,
deliberate choices.
Every choice keeps the current learning loop understandable while leaving a clear path for the next constraint.
A public demo box invites unauthorized access. One username/password guards every route — pages, assets, the practice WebSocket — compared via hashed constant-time compare, behind a TLS-terminating proxy.
A shared box means the app must never hold the real socket. The clerk forwards only container create/start/stop/remove — images, volumes, secrets, and Swarm stay unreachable.
Containers are created fresh per session and removed afterward — nothing persists, nothing leaks between students, nothing lingers to be exploited.
Code default is 5 sessions; production runs 2 because the host actually has 2 CPU cores.
The application needed to create, attach to, stop, and remove containers programmatically.
Same build → tag → push → verify → auto-deploy path the other four apps on this host already use.
Local setup stayed lightweight while a typed data layer keeps a future PostgreSQL move bounded.
Learning pages are content-focused and do not require a large frontend framework.
Appropriate for demo-scale protection, with a clear path to shared storage later.
Routing and rendering needs were small enough to avoid unnecessary framework complexity.
The project proves the learning loop before introducing multi-host orchestration.
Honest about
the path forward.
A focused single-host system is the right starting point. These are deliberate next steps, not promises hidden behind a polished interface.