Gengie, Part 1: Picking OCaml for a Game Engine
Why I'm learning OCaml by building a tiny WebAssembly game engine — and the one closure trick that made the type system click.
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.
Written By Alinus Dumitrana
May 25, 2026 • 4 min read
Every new project started the same way: copy the last devcontainer.json, rip out half of it, paste in the new stack, fight with the Dockerfile for an afternoon, swear at mise for not being on PATH, then commit something I knew would rot.
Eventually I got tired enough to do it properly. The result is alindesign/devcontainers — a set of reusable Dev Container Features and Templates, published to ghcr.io/alindesign, that I now drop into every new repo.
This is the story of what I was actually trying to solve, the bug that taught me the most, and what I'd tell someone before they started.
Dev Containers are great in theory: describe your environment once, anyone who opens the project gets the same setup. In practice, the average devcontainer.json ends up either copy-pasted between repos and silently diverging, or bloated into a 200-line Dockerfile that nobody understands six months later.
I wanted three things:
fd, rg, bat, fzf, delta, eza, zoxide), my git signing setup, my starship prompt. Versioned once, reused everywhere.aws, gcloud, and Claude Code to just work when I open the container — not "after you run three manual login commands."Features and Templates were the right primitive. A Feature is a small reusable install script with a JSON manifest; a Template is a starter devcontainer.json that wires features together. Both publish as OCI artifacts on a registry, so consuming them is just a string in a JSON file.
Seventeen features, twelve templates. The features split into three buckets:
dotfiles (shell + CLI tools + nvim + git), mise (toolchain manager).node, go, rust, java, ocaml, python, bun. All but rust, ocaml, bun go through mise so versions are pinned per-project.docker-in-docker, kubectl (+ helm, k9s, kubectx), opentofu, aws-cli, gcloud, ansible, db-tooling (psql/mysql/redis-cli/mongosh), secrets (sops + age + pass), claude (Claude Code CLI).Templates are just opinionated bundles: claude-dev for AI-assisted work with my host credentials mounted in, cloud-ops for multi-cloud work, devops as the full IaC toolbox, k8s, data-science, plus one per language.
The whole thing builds on a pre-baked base image, ghcr.io/alindesign/devcontainer-base, with dotfiles + mise already burned in — so the common case skips two layer rebuilds.
Most features looked great on my laptop. The trouble started in CI.
I'd shipped aws-cli, gcloud, and claude features with bind mounts declared right in the feature manifests: ~/.aws, ~/.config/gcloud, ~/.claude. The intent was nice and tidy. Add the feature, get your host credentials inside the container automatically. Worked perfectly for me, since those directories obviously existed on my machine.
On a fresh CI runner without them, container startup just died with a cryptic OCI runtime error. Bind mounts assume the host path exists. They don't know or care that you might not be on the developer's laptop.
The fix wasn't really about the error message; it was about the boundary. I moved mounts out of the feature manifests and into the templates (claude-dev, cloud-ops), where the user has explicitly opted in to host integration. Features now stay portable across any environment. Templates carry the host wiring for the cases where it makes sense.
The follow-on touch I'm most proud of: the claude-dev template uses initializeCommand on macOS to pull the Claude Code OAuth token from the Keychain and write it to ~/.claude/.credentials.json before the container starts. The container is authenticated on first boot, no manual claude login required. Small detail, disproportionate quality-of-life payoff.
Composition beats configuration. A devcontainer.json that lists 5 features and stops is more maintainable than a 200-line Dockerfile, even if each individual feature is more complex internally. The cognitive load is paid once, by me, and amortized across every project.
OCI as a package registry is underrated. I push features and templates to GHCR the same way I'd push container images. No npm-style registry, no special tooling, just docker push. The devcontainer CLI fetches by OCI ref. It's clean.
Build for someone else's machine, not yours. The mounts bug only existed because every assumption about host state lived inside the feature. Once I started treating features as code that runs on a CI runner I've never seen, the design got sharper. The same instinct applies to most "infrastructure" code I write now.
Test the failure modes, not just the happy path. My CI matrix tests every feature against the Microsoft base image. The bugs that mattered only showed up in environments that weren't mine: a runner without my dotfiles, a different remote user, a missing host directory. Tests in your own environment confirm what you already believe.
AI did a lot of the heavy lifting. A large chunk of the install scripts, the GitHub Actions matrix, and the test scenarios were written with Claude Code. It's also why claude-dev was the first template I shipped: I wanted that workflow inside a clean, reproducible container, not bolted onto whatever the host happens to look like.
The library covers most of my day-to-day work now: navigating code, running Node and Go apps, building web apps, and the bits of devops that creep into every project. I'll keep adding features as I bump into new tooling, but the core feels stable.
If you've been copy-pasting devcontainer.json files between projects, build your own library. It's three afternoons of work and pays itself back the next time you start a project.
Source: github.com/alindesign/devcontainers.
Why I'm learning OCaml by building a tiny WebAssembly game engine — and the one closure trick that made the type system click.
Lessons from building an AI-powered product — function calling, prompt management, and the gap between demos and production systems.