case study · go · linux · systems
Container Runtime — what “a container” actually is, in raw syscalls
A Linux container runtime written from first principles in Go, with zero third-party dependencies. No Docker, no libcontainer, no ip(8) — just namespaces, cgroups v2, pivot_root, and hand-built netlink talking directly to the kernel. Built to make the isolation mechanism visible instead of hiding it behind a daemon.
The problem
A container feels like a magic box, but there is no such thing as a container in the Linux kernel. There are only namespaces that hide part of the system from a process, cgroups that cap what it can consume, and a root filesystem that has been swapped out from under it. Docker composes those primitives and buries them under a daemon. I wanted the opposite: to build a runtime where every isolation boundary is a syscall I wrote by hand, so “what is a container” stops being a metaphor. The self-imposed rule was zero third-party Go dependencies — everything runs on the standard library, which forces the interesting parts to stay in view instead of being delegated to a library.
What I built
/proc/self/exe init, because namespace creation needs a fresh single-threaded process that Go’s multithreaded runtime can’t give you inlineFilesystem isolation via pivot_root(2) — mounts made private, the new root bind-mounted onto itself, the old root pivoted away and detached — chosen over chroot because chroot is escapableA complete cgroup v2 manager that creates a per-container group, enables controllers through cgroup.subtree_control, and writes cpu.max, memory.max, and pids.max directly to sysfsLoopback brought up inside the network namespace through a hand-rolled raw rtnetlink message — an RTM_NEWLINK built byte by byte, sent over an AF_NETLINK socket, and its ACK parsed — with no netlink library and no shelling out to ipUser-namespace UID/GID remapping (container root → an unprivileged host range) and a shared OCI-flavored specs contract layer that the subsystems agree onDocumentation as a first-class artifact: six architecture decision records, a threat model, and an honest implemented-vs-scaffolded status tableEngineering decisions
Cloneflags and re-runs its own binary as an init child rather than trying to clone() in place — the same pattern real runtimes use, and the reason for it only becomes obvious once you hit the bug.Hand-roll rtnetlink rather than import a library. Bringing up lo meant constructing a netlink message with unsafe sizing, sending it over a raw socket, and decoding the kernel’s ACK. It is the most involved code in the project, and doing it by hand is the whole point — the dependency you don’t add is the thing you actually learn.pivot_root over chroot. chroot can be escaped through open file descriptors and fchdir. Making the mount tree private first, then pivoting and detaching the old root, gives a boundary that doesn’t leak back to the host — a security rationale that lives in the code, not just the commit message.cgroup v2 only, written straight to sysfs. No systemd, no v1/hybrid fallback. The manager refuses to remove a group that still lists live PIDs, and reads back memory.current and OOM-kill counts from memory.events — treating the kernel’s filesystem interface as the API it is.Where it stands, honestly
This is a milestone project, not a finished engine. The isolation primitives are real, tested, and the reason the project exists — the six-namespace launch, pivot_root, the cgroup v2 manager, and the loopback netlink path all work and are covered by unit and root-gated integration tests. The layers that turn primitives into a product — image pull and overlay filesystems, bridge/veth networking, and the full create/start/stop lifecycle — are scaffolded with typed interfaces and marked clearly as not-yet-implemented. I chose to document that boundary in an explicit status table rather than paper over it, because a runtime that quietly pretends to isolate is worse than one that tells you exactly how far the isolation goes.
What building it taught me is that a container is not one feature but a stack of independent kernel mechanisms that only add up to “isolation” when every one of them is set up correctly and in the right order. Skip the private mount propagation and your pivot_root leaks to the host; forget to write the UID map and your “root” is really nobody. Seeing those failure modes first-hand is worth far more than any amount of reading about them.