a container runtime in ~1,200 lines of Go. no container libraries — just syscalls, a lot of man pages, and the OCI spec for company.
I didn't build this because the world needed another runtime. runc exists, it's battle-tested, and it runs most of the internet's containers.
I built it because I wanted to know what Docker actually does, and reading only gets you so far — at some point you have to hold the knife.
There's also a second half I haven't built yet. the plan was always jail + warden: burg is the jail (namespaces, mounts, pivot_root, eventually cgroups), and the warden is a BPF-LSM policy engine that enforces per-container rules at runtime — a mini Tetragon. that's v2, and it's the actual reason burg exists.
The first surprise came early. I had a running container, I sent it SIGTERM, and — nothing. not an error, not a delay, nothing. SIGKILL worked fine. so somewhere between TERM and KILL, the kernel was quietly eating my signal.
Turns out the kernel protects PID 1 — the init process of any PID namespace — from every signal it hasn't explicitly installed a handler for. My child process became PID 1 by exec'ing straight into the user's program, so every polite signal was discarded, and only SIGKILL (which ignores everyone's feelings) got through.
The fix is the same one runc uses: never let the user's program be PID 1. burg runs a small supervisor as PID 1 that catches every signal and forwards it, and launches your program as PID 2 — a normal citizen that dies when asked. You can see it from inside the container:
/ # ps
PID USER TIME COMMAND
1 root 0:00 /proc/self/exe child-init /home/.../demo-bundle
8 root 0:00 sh
11 root 0:00 ps
| bundle parsing | config.json parsed and validated against the required fields |
| namespaces | pid, mount, uts, ipc, network, cgroup — via clone flags from the spec |
| mounts | proc, tmpfs, devpts, sysfs, mqueue, bind — spec-driven, in order |
| rootfs | pivot_root, put_old unmounted and removed, read-only root when root.readonly is set |
| lifecycle | create / start / run / state / kill / delete — all real, no stubs |
| supervising init | signal forwarding to the container process |
| readiness pipe | the parent won't write running to the state file until the child confirms it's actually up — a container that died during setup never gets a lying state file |
| exit codes | the container's exit code becomes burg's exit code (exit 42 inside → echo $? → 42; killed → 128+N) |
burg spec |
drops a runnable default config.json so you don't hand-write one |
- cgroups v2 — the hierarchy code exists and is tested, it's just not wired into create/start yet. v1.1.
- capabilities, rlimits, maskedPaths, readonlyPaths — parsed by the spec types, silently ignored by burg.
- networking — a new network namespace with nothing inside it. no veth, no bridge.
- pty —
terminal: truemeans inherited stdio, not a real pty. no resize events, and Ctrl-C hits burg too. - hooks, seccomp, apparmor, rootless,
kill --all— not started.
and it all runs as root with full capabilities. don't put anything you like near it.
mkdir demo && cd demo && mkdir rootfs
tar -xzf ~/Downloads/alpine-minirootfs-*.tar.gz -C rootfs # or busybox
burg spec # writes config.json — edit hostname/args if you like
sudo burg run mybox -b .
inside: hostname, ps, cat /proc/mounts, ls / — then from another terminal:
sudo burg state mybox # the OCI state, as JSON
sudo burg kill mybox KILL # TERM is forwarded, but shells may ignore it — KILL never fails
sudo burg delete mybox
sequenceDiagram
participant P as burg (parent)
participant S as supervisor — PID 1
participant U as program — PID 2
P->>S: fork into fresh namespaces
S->>S: hostname, mounts, pivot_root, ro remount
S->>U: fork
S-->>P: "1" on the pipe (fd 3) — I'm up
P->>P: save state: running
Note over S: burg kill TERM arrives
S->>U: forward signal
U-->>S: exit (own code, or 128+N)
S-->>P: exit with the same code
P->>P: save state: stopped
- ~5 ms cold start — full lifecycle of an exit-0 container, 5.3 ms ± 0.6 over 30 runs (hyperfine, shell-free), CPU pinned at 3.4 GHz on two P-cores, kernel 7.2.0. reproduced with
scripts/bench.sh - 1,238 lines of runtime Go (scc, tests excluded)
- 2,168 lines of tests — the suite outweighs the runtime ~1.75x, which is how I like it
- dependencies: cobra (CLI),
runtime-spec(JSON types only),x/sys(raw syscalls). nothing that knows what a container is
- v1.1 — wire cgroup v2 limits into create/start/delete
- v2 — the warden: a BPF-LSM policy engine enforcing per-container syscall/file/network rules at runtime
not on the roadmap: being runc. see the missing table.
license: GPL-3.0. it's a learning project — take it apart, that's the point.
