Building a Dev Container Library: Lessons from Shipping 17 Features
Why I built a personal library of Dev Container Features and Templates, the sharp edges I hit along the way, and what shipping to GHCR taught me about reproducible dev environments.
Why I'm learning OCaml by building a tiny WebAssembly game engine — and the one closure trick that made the type system click.
Written By Alinus Dumitrana
May 25, 2026 • 4 min read
I'm building a tiny game engine for the browser, in a language I didn't know two days ago. The project is called Gengie — "Game" plus "engine" — and it draws to a single <canvas> via brr, OCaml's binding to the browser platform.
This post isn't a tour of OCaml. There are good tours. This is the one moment in the first two days where the language stopped feeling like a foreign object in my hands.
I picked the project before I picked the language. I wanted something with a tight feedback loop (a square that moves when I press a key), a real toolchain decision to make (how does this thing end up in a browser at all?), and enough surface area to keep going for months without running out of new ideas. A small game engine fits.
OCaml came next, for two reasons:
brr exists, and it's good. It's a thoughtful OCaml binding to the browser platform — DOM, events, canvas, the lot. Calling requestAnimationFrame from OCaml feels like writing OCaml, not like wrestling FFI.Rust would have been easier. That's why I didn't pick it.
A scene in Gengie is a list of objects. Each object has its own state, its own update logic, its own render code. A moving square holds a position and velocity. A stats HUD holds nothing. An FPS counter holds nothing either but renders different text. They all need to live in the same list and tick on the same frame.
In a language with classes I'd reach for an interface and a List<GameObject>. In Rust I'd reach for Box<dyn GameObject> and a trait. In OCaml — having no idea how this should work — I asked the obvious question: how do you put values of different types into one list?
The answer is in 14 lines of lib/object.ml:
type t = { tick : Frame.t -> t; draw : Frame.t -> Canvas.t -> unit } let make ~init ~update ~render = let rec build state = { tick = (fun frame -> build (update state frame)); draw = (fun frame canvas -> render state frame canvas); } in build init let step t frame = t.tick frame let render t frame canvas = t.draw frame canvas
Object.t is a record of two functions. That's it. There's no 'state parameter on the type. The state type only exists inside make, where it's bound by the 'state argument. After make returns, the only things that still know about it are the closures captured inside tick and draw.
From the outside, every Object.t looks identical. From the inside, one holds { pos; vel }, another holds unit, another holds an int. They sit in the same Object.t list and the type checker is fine with it.
This is the closure-as-existential pattern. The language doesn't need a separate concept for it. No dyn, no interface, no abstract class. The function type is the interface, and the closure is the box. When that landed, a lot of other things in OCaml stopped looking strange — modules, functors, the way the standard library is shaped. It's all the same instinct: hide the thing you don't want callers to see, and let the type system do the rest.
I'd seen the pattern described before. Reading about it didn't do anything. Watching it solve a problem I actually had — a list of objects with different state shapes that need to tick together — is when it became a tool I'd reach for again.
What's in the engine right now is small: a frame loop, a scene, an object type, a canvas wrapper, a keyboard input snapshot, a vec2, a color type, a shared state container, an FPS counter that smooths with an exponential moving average. The demo is a square you can move around with WASD, with an HUD that ticks up every time it hits a wall.
That's enough to have surfaced every interesting decision so far. How to organize modules. Where to put shared mutable state. How to model "per-frame inputs" cleanly when the underlying browser API is event-based. None of those answers are final, but they're all written down in OCaml now, and I can change them.
I'm using Claude Code as a tutor. The workflow that's actually working is narrow: I pick the next small feature, ask for the minimal idiomatic version, read the explanation carefully, then retype it from scratch and try to break it.
The retyping matters. If I just accept the code as-is, the syntax goes through my eyes without touching anything. If I retype it, I have to make every small decision again — the operator precedence, the labeled arguments, the order of match arms — and the second time around I notice the pieces I was glossing over. Reading is comprehension; retyping is learning.
AI is good at explaining why an idiom is idiomatic. It's bad at telling me which idea has actually clicked in my head. That part has to happen at the keyboard.
The engine is a means, not the end. I want to build a small indie game on top of it — something simple enough to actually finish, but real enough to push the engine into shapes a moving square can't reach. Tilemaps, sprites, audio, scene transitions. The first thing on that path is probably a tilemap, which forces a question I've been avoiding: how do I want asset loading to work from a browser build? When that has an answer, there'll be a Part 2.
Why I built a personal library of Dev Container Features and Templates, the sharp edges I hit along the way, and what shipping to GHCR taught me about reproducible dev environments.
Lessons from building an AI-powered product — function calling, prompt management, and the gap between demos and production systems.