Personal project
Wharf
A Linux container runtime in Python with namespaces, an overlay root filesystem, cgroup v2 limits and a Docker-style CLI.
wharf
$ wharf pull tools:latest --host /bin/sh /bin/echo /bin/sleep /bin/ls
tools:latest
$ wharf run tools:latest /bin/echo hello from a container
hello from a container
$ wharf run -d --name nap --memory 64m --cpu 0.5 --pids 64 tools:latest /bin/sleep 300
0cb542f06c8a
$ wharf ps
id name image status pid command
0cb542f06c8a nap tools:latest running 54372 /bin/sleep 300
$ wharf exec nap /bin/ls /proc/1/ns
cgroup ipc mnt net pid pid_for_children time time_for_children user uts
$ wharf stop nap
0cb542f06c8a exited
Problem
Containers are often explained as lightweight VMs, but they're ordinary Linux processes with a restricted view of the system. I wanted to understand that by building the whole path myself, from unshare to a running process in its own root filesystem.
What I built
Wharf gives a process its own user, mount, UTS, PID, IPC and network namespaces, then pivots it onto an overlay root filesystem. It stores images locally and enforces cgroup v2 memory, CPU and pids limits.
A Docker-style CLI covers pull, run, ps, exec, logs, stop and more, and a small control page lists what is running.
How
- CLIrun, ps, exec, logs, stop
- SupervisorUnshares a user namespace and writes the id map
- NamespacesMount, UTS, PID, IPC, networkcgroup v2memory.max, cpu.max, pids.max
- Overlay rootfsOrders lowerdirs, applies OCI whiteouts
- pivot_rootMounts, pivots and detaches the old root
- Container processsetns lets exec join it later
The supervisor unshares a user namespace first and maps container root onto the calling user, so run needs no root when unprivileged user namespaces are enabled.
pivot_root is called directly as a syscall, exec enters a running container with setns, and layers resolve in overlay order with OCI whiteouts applied.
Results
- It runs real containers end to end: detached workloads with memory, CPU and pids limits,
execinto a live container, and clean stops. - The full
psandexecflow above was run from a fresh clone on WSL2. - The limits are explicit: there is no registry client, daemon, seccomp profile, capability bounding set or veth bridge. This is a learning runtime, not Docker.
What I learned
- Order is everything. The id map, mounts, pivot and cgroup writes each depend on what has already happened, and getting one out of order fails in confusing ways.
- Designing for rootless use forced me to learn which privileges a container really needs.