A minimal, fully-functional Linux container runtime built from scratch in Go โ demonstrating deep knowledge of namespaces, cgroups v2, OverlayFS, networking, OCI images, and observability.
MVP 1 โ MVP 2 โ MVP 3 โ MVP 4 โ MVP 5 โ MVP 6
NS cgroups Overlay Network CLI+Pull TUI
Most engineers use Docker daily without understanding what it actually does. This project builds a real container runtime from first principles. Every feature maps directly to a core Linux concept
- Linux kernel โฅ 5.10 (cgroup v2 required)
- Go 1.21+
- Root / sudo access (namespaces and cgroups require it)
iptablesandnsenterinstalled
# 1. Clone and build
git clone <repo-url> && cd gocker
make build
# 2. Set up the Alpine root filesystem
sudo make rootfs
# 3. Enable IP forwarding for container NAT
sudo make setup-nat
# 4. Run your first container
sudo ./gocker run /bin/sh
# 5. Run an image from Docker Hub
sudo ./gocker pull redis
sudo ./gocker run -it redis /bin/shcmd/
gocker/
main.go โ CLI entrypoint (run, ps, stop, rm, pull, exec, logs, top)
internal/
container/
namespace.go โ clone() flags, pivot_root, hostname, /proc + /dev + /tmp mounts
cgroup.go โ cgroup v2 setup / teardown (memory, cpu, pids)
fs.go โ OverlayFS mount / unmount
network.go โ veth pairs, bridge, iptables NAT + port mapping
image.go โ image ID helpers
image/
pull.go โ Docker Registry v2 API client
layers.go โ layer tar extraction with whiteout support
state/
store.go โ JSON container state (/var/lib/gocker/state.json)
metrics/
poller.go โ 500ms cgroup + /proc metric poller
buffer.go โ 60-sample circular buffer per metric
tui/
app.go โ bubbletea root model โ tabbed dashboard (Containers / Logs / Info)
styles.go โ lipgloss dark-navy palette, tab/chip/badge/panel styles
sparkline.go โ Unicode block chart renderer (bars + sparklines)
tests/
mvp1_namespace_test.go โ PID/hostname/FS/process isolation
mvp2_cgroup_test.go โ memory OOM, pids limit, CPU limit, cleanup
mvp3_overlay_test.go โ overlay mount, write isolation, /proc
mvp4_network_test.go โ bridge, container IP, outbound NAT
mvp5_e2e_test.go โ pull, run, ps, logs
mvp6_tui_test.go โ circular buffer, history, poller (unit tests)
Linux concepts: clone(), CLONE_NEWUTS, CLONE_NEWPID, CLONE_NEWNS, CLONE_NEWIPC, CLONE_NEWNET, pivot_root, /proc mount
sudo ./gocker run /bin/sh
# Inside the container:
echo $$ # โ 1
hostname # โ <container-id>
ls / # โ alpine rootfs, not host
ps aux # โ only container processesImplementation notes:
- We re-exec
/proc/self/exe child ...to apply namespaces before the user command starts exec.LookPathMUST NOT be called beforepivot_rootโ it searches the host PATH/devis a fresh in-memorytmpfswith hand-crafted minimal device nodes โ the host/devis never bind-mounted (avoids SIGWINCH leaks)/tmpis mounted astmpfswithmode=1777(sticky bit) so privilege-dropping subprocesses (e.g.apt's GPG helper) can usemkstemp()withoutEACCES/runand optionally/var/runalso get freshtmpfsmounts for daemon lock files- The rootfs path is configurable via
GOCKER_ROOTFSenv var (default:/var/lib/gocker/rootfs/alpine)
Linux concepts: cgroup v2 unified hierarchy, memory.max, cpu.max, pids.max, cgroup.procs
sudo ./gocker run --memory 64m --cpus 0.5 --pids-limit 20 /bin/sh| File | Purpose |
|---|---|
memory.max |
Hard memory cap (bytes) |
cpu.max |
quota period โ e.g. 50000 100000 = 50% of one core |
pids.max |
Max processes in the container |
cgroup.procs |
Enroll a PID |
Cleanup: TeardownCgroups polls until cgroup.procs is empty before rmdir (kernel returns EBUSY otherwise).
Linux concepts: OverlayFS, copy-on-write, lowerdir, upperdir, workdir, merged
# Directory layout
/var/lib/gocker/
images/<image-id>/rootfs/ โ lower (read-only base layers)
containers/<id>/
upper/ โ container writes (copy-on-write)
work/ โ overlayfs internal
merged/ โ final view used as rootfsEach container gets its own upper/ so writes in container A are invisible to B.
Linux concepts: Linux bridge, veth pairs, network namespaces, iptables MASQUERADE (NAT), DNAT (port mapping)
sudo make setup-nat # enable ip_forward
sudo ./gocker run -p 8080:80 /bin/sh
# Inside:
ip addr # โ 10.0.0.x/24
ping 8.8.8.8 # outbound NAT worksNetwork setup per container:
gocker0bridge created at10.0.0.1/24vethpair: host end โ bridge, container end โ container net namespace- IP
10.0.0.xassigned inside container - Default route โ
10.0.0.1 iptables -t nat -A POSTROUTING -j MASQUERADEfor outboundiptables -t nat -A PREROUTING -j DNATfor port mappingiptables -I FORWARD -i gocker0 -o gocker0 -j ACCEPTfor inter-container traffic
Linux concepts: Docker Registry v2 API, OCI image format, setns() for exec
# Pull from Docker Hub
sudo ./gocker pull alpine
sudo ./gocker pull redis
sudo ./gocker pull nginx
# Run using pulled image
sudo ./gocker run alpine /bin/sh
sudo ./gocker run -it redis /bin/sh
# Container management
sudo ./gocker ps -a
sudo ./gocker stop <id>
sudo ./gocker rm <id>
sudo ./gocker exec <id> /bin/sh
sudo ./gocker logs <id>State store: /var/lib/gocker/state.json tracks ID, image, command, status, PID, IP, ports, timestamps.
OCI pull flow: anonymous token โ manifest v2 โ layer blobs (gzip tar) โ SHA256 verify โ extract with whiteout support.
Libraries: bubbletea, lipgloss
sudo ./gocker topThe dashboard uses a professional dark-navy theme built entirely with Lip Gloss:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ 21:03:42 โโ
โ ๐ณ gocker top โ โ header + live clock
โโโ[Containers]โโ[Logs]โโ[Info]โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค โ tab bar
โ TOTAL 3 โ RUN 2 โ EXIT 1 FILTER: 5m โ โ summary chips
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ID IMAGE STATUS CPU MEM NET โ โ table header
โ a1b2c3d4 alpine โ RUN โโโโโ 54% 32 MB โ1KB โ
โ> b2c3d4e5 nginx:latest โ RUN โโโโโ 12% 56 MB โ4KB โ โ selected (blue)
โ c3d4e5f6 redis โ STOP โโโโ โ โ โ โ exited (dim)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โญโ INSPECT nginx:latest (b2c3d4e5) โโโโโโโโโโโโโโโโโโโโโโโฎ โ
โ โ Status โ RUN CPU 12.3% CPU history (30s) โ โ
โ โ IP 10.0.0.3 Mem 56.1MB โโโโโ
โโโโโ โ โ
โ โ Uptime 4m32s TX 4.1 KB Mem history (30s) โ โ
โ โ PID 1234 RX 1.2 KB โโโโ
โ
โโโโโ โ โ
โ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ [/] search [โโ/jk] navigate [Tab] switch tab [q] quit โ โ footer
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Keyboard bindings:
| Key | Action |
|---|---|
โ / k |
Move selection up |
โ / j |
Move selection down |
Tab |
Cycle tabs (Containers โ Logs โ Info) |
Shift+Tab |
Cycle tabs backwards |
/ |
Open search overlay |
a |
Toggle show-all (include exited > 5m) |
1 2 3 4 |
Sort by image / CPU / mem / status |
r |
Force refresh container list |
q / Ctrl+C |
Quit |
Architecture (3 goroutines):
Poller goroutine โโmetricsโโโถ CircularBuffer โโrenderโโโถ bubbletea renderer
(500ms ticker) (60 samples) (lipgloss layout)
We use the official Alpine Linux minirootfs tarball (~5 MB). Here's why:
| Option | Size | Pros | Cons |
|---|---|---|---|
| Alpine minirootfs โ | ~5 MB | Tiny, fully offline after setup, has sh/apk |
Must be extracted once |
| BusyBox static | ~1 MB | Smallest possible | No package manager |
| Ubuntu base | ~100 MB | More tools | Very large |
| Dynamic OCI pull | Varies | Any image | Needs network + MVP 5 |
make rootfs downloads the Alpine 3.19 x86_64 minirootfs from the official CDN and extracts it to /var/lib/gocker/rootfs/alpine. MVP 5 adds the ability to pull and run any Docker Hub image dynamically.
# Build the binary first
make build
# Set up rootfs
sudo make rootfs
# Unit tests only (safe, no root)
make test-unit
# All integration tests (requires root + rootfs)
sudo make test
# Individual MVPs
sudo go test ./tests/ -run TestMVP1 -v
sudo go test ./tests/ -run TestMVP2 -v
go test ./tests/ -run TestMVP6 -v # unit tests, no root needed| Test | What it checks | Root? |
|---|---|---|
TestMVP1_PIDIsolation |
Container sees PID 1 | โ |
TestMVP1_HostnameIsolation |
Hostname = container ID | โ |
TestMVP1_FilesystemIsolation |
Alpine rootfs visible inside | โ |
TestMVP2_MemoryLimit |
OOM kills process over limit | โ |
TestMVP2_PidsLimit |
Fork fails when limit exceeded | โ |
TestMVP2_CPULimit |
CPU-limited container exits cleanly | โ |
TestMVP2_CgroupCleanup |
No orphan cgroup dirs after exit | โ |
TestMVP3_OverlayMount |
Alpine rootfs visible via overlay | โ |
TestMVP3_WriteIsolation |
Host rootfs not polluted | โ |
TestMVP3_ProcMount |
/proc mounted and usable | โ |
TestMVP4_BridgeExists |
gocker0 bridge present | โ |
TestMVP4_ContainerHasIP |
Container gets 10.0.0.x | โ |
TestMVP5_PullAndRun |
Pull alpine, run echo hello | โ |
TestMVP6_CircularBuffer |
Ring buffer overflow correct | โ |
TestMVP6_ContainerHistory |
Metrics pushed to all buffers | โ |
| MVP | Question it answers |
|---|---|
| 1 โ Namespaces | "How do containers differ from VMs?" |
| 2 โ cgroups | "How does Docker enforce memory limits?" |
| 3 โ OverlayFS | "How does Docker's image layering work?" |
| 4 โ Networking | "How does container networking work?" |
| 5 โ CLI + Pull | "Can you ship a usable tool end-to-end?" |
| 6 โ TUI Dashboard | "How do you approach observability tooling?" |
clone()withCLONE_NEW*namespace flagspivot_root+ mount namespace manipulationtmpfsmounts for/dev,/tmp(sticky 1777),/runโ all ephemeral and isolated- cgroups v2 unified hierarchy via
/sys/fs/cgroup/ - OverlayFS:
lowerdir,upperdir,workdir,merged - Linux bridge devices and
vethpairs - iptables
MASQUERADE(NAT) andDNAT(port mapping) setns()for joining existing namespaces (execcommand)- Docker Registry v2 API + OCI image format
/proc/<pid>/stat,/proc/<pid>/io,/proc/<pid>/net/devfor live metrics- Circular buffer for time-series metric history
- TUI architecture with bubbletea (Elm model/update/view pattern)
- Lip Gloss layout composition (
JoinVertical,JoinHorizontal, style chaining) - Unicode block characters for terminal bar and sparkline charts