tiny-docker-go is a small container runtime written in Go to learn how Docker-like systems are built from Linux primitives. It can start a process in new namespaces, chroot into a root filesystem, apply cgroup v2 memory limits, persist container metadata, stream logs, stop containers, and provide simple bridge-based networking.
This is a teaching project, not a production container engine. The focus is clarity, incremental implementation, and understanding the control flow from CLI to runtime.
- Linux container execution with UTS, PID, mount, and network namespaces
chroot-based root filesystem isolation- cgroup v2 memory limits with
--memory - container metadata and logs under
/var/lib/tiny-docker/containers ps,logs,logs -f, andstoplifecycle commands- bridge networking with a host bridge, veth pair, and NAT
isolatedandnonenetwork modes- macOS support through a Lima-managed Linux VM with the same CLI
tiny-docker-go CLI
-> internal/cli
-> internal/app
-> internal/runtime service
-> metadata store creates container record
-> parent process re-execs /proc/self/exe as "child"
-> child enters Linux namespaces
-> hostname / mount / network / chroot setup
-> target command runs inside the container context
-> logs stream to terminal + container.log
-> metadata updates to running / exited / stopped
tiny-docker-go/
├── cmd/tiny-docker-go/main.go # program entrypoint
├── internal/app/app.go # app wiring
├── internal/cli/ # command parsing and help text
│ ├── command.go
│ ├── run.go
│ ├── ps.go
│ ├── logs.go
│ ├── stop.go
│ └── child.go
└── internal/runtime/ # container runtime implementation
├── service_linux.go # main Linux execution path
├── service_unsupported.go # non-Linux behavior
├── network_linux.go # bridge, veth, namespace network setup
├── cgroup_linux.go # cgroup v2 memory limits
├── metadata_store.go # config/log persistence
└── service.go # shared runtime types
container process
-> eth0 in container netns
-> veth peer
-> td0 bridge on host
-> host routing + iptables MASQUERADE
-> external network
- Go 1.22+
- Linux for native
runsupport, or macOS with Lima - root privileges for namespaces, mounts, networking, and cgroups
ipfromiproute2iptablesfor outbound NAT inisolatedmode- cgroup v2 mounted at
/sys/fs/cgroup - a root filesystem to run inside, such as Alpine unpacked locally
Build for the current machine:
go build ./...Build the Linux binary explicitly:
GOOS=linux GOARCH=amd64 go build -o tiny-docker-go ./cmd/tiny-docker-goBuild the Linux binary artifact expected by the macOS Lima backend.
Use amd64 on Intel Macs and arm64 on Apple Silicon Macs:
mkdir -p bin
GOOS=linux GOARCH=amd64 go build -o ./bin/tiny-docker-go-linux-amd64 ./cmd/tiny-docker-goOn unsupported non-Linux and non-macOS platforms, the project still builds, but run returns a platform error by design.
On macOS, tiny-docker-go keeps the same CLI and forwards execution into a Linux VM managed by Lima.
Requirements:
- Lima installed with
limactlavailable inPATH - a running Lima instance named
tiny-docker - the project workspace shared into the VM
- the Linux binary built at
./bin/tiny-docker-go-linux-<host-arch>such as./bin/tiny-docker-go-linux-arm64on Apple Silicon - a
rootfspath that is visible inside the shared workspace mount
Example setup:
limactl start --name=tiny-docker template:default
mkdir -p bin
GOOS=linux GOARCH=arm64 go build -o ./bin/tiny-docker-go-linux-arm64 ./cmd/tiny-docker-go
go build -o ./bin/tiny-docker-go-darwin ./cmd/tiny-docker-goThen use the macOS binary with the normal commands:
./bin/tiny-docker-go-darwin run --rootfs ./rootfs/alpine /bin/sh
./bin/tiny-docker-go-darwin ps
./bin/tiny-docker-go-darwin logs <container-id>
./bin/tiny-docker-go-darwin stop <container-id>Notes:
- metadata, logs, and process state stay in the Linux VM environment
- the macOS binary is only a transport layer into Lima
- runtime commands are executed with
sudoinside the guest so they can use Linux namespaces, mounts, and cgroups - if a shared path is not visible inside the VM, the command fails early with a clear error
One simple workflow is to unpack Alpine into ./rootfs/alpine on a Linux host:
mkdir -p rootfs/alpine
curl -L https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/alpine-minirootfs-latest-x86_64.tar.gz \
| sudo tar -xz -C rootfs/alpine./tiny-docker-go help
./tiny-docker-go help runsudo ./tiny-docker-go run --rootfs ./rootfs/alpine /bin/shOn macOS with Lima:
./bin/tiny-docker-go-darwin run --rootfs ./rootfs/alpine /bin/shsudo ./tiny-docker-go run --hostname demo --rootfs ./rootfs/alpine /bin/shsudo ./tiny-docker-go run --memory 128m --rootfs ./rootfs/alpine /bin/shsudo ./tiny-docker-go run --net isolated --rootfs ./rootfs/alpine /bin/shsudo ./tiny-docker-go run --net none --rootfs ./rootfs/alpine /bin/shsudo ./tiny-docker-go ps
sudo ./tiny-docker-go ps -aThe ps table includes container status, PID, creation time, network mode, memory limit, and command.
sudo ./tiny-docker-go logs <container-id>
sudo ./tiny-docker-go logs -f <container-id>sudo ./tiny-docker-go stop <container-id>Each container gets a generated ID and a directory:
/var/lib/tiny-docker/containers/<id>/
├── config.json
└── container.log
config.json currently stores:
idcommandhostnamerootfsmemory_limitnetwork_modeip_addressstatuscreated_atpid
- native container execution still requires Linux primitives inside the VM or on a Linux host
- requires root instead of rootless execution
- uses
chroot, notpivot_rootor overlay filesystems - no image format, image pull, or layered filesystem support
- no bind mounts, named volumes, or writable snapshot management
- no port publishing from host to container
- no background or detached containers
- no DNS management inside the container rootfs
- limited resource controls beyond memory
- no seccomp, capabilities dropping, AppArmor, SELinux, or user namespaces
- no TTY/interactive terminal management beyond basic stdio wiring
- metadata is local file storage only and not crash-resilient like production runtimes
- macOS execution depends on a preconfigured Lima instance and a shared workspace layout
- detached containers and a
start/rmlifecycle - port publishing such as
-p 8080:80 - bind mounts and writable container filesystems
- better error messages and validation around Linux host prerequisites
pivot_root-based filesystem setup- overlay filesystem support
- user namespaces and rootless mode experiments
- seccomp profiles and capability dropping
- CPU and PID cgroup controls
- minimal image format and local image cache
- embedded DNS and
/etc/resolv.confmanagement - container checkpointing or snapshot experiments
If you want to keep pushing this into a stronger systems project, these are the highest-value next steps:
- Add detached containers plus persistent lifecycle commands.
- Replace
chrootwith a safer mount-tree flow using bind mounts andpivot_root. - Add port publishing and explicit cleanup for NAT and forwarding rules.
- Introduce bind mounts and a writable layer so containers can host realistic workloads.
- Add security controls: user namespaces, capability dropping, and a small seccomp profile.
- Expand cgroups beyond memory to CPU, PIDs, and I/O controls.
- Add rootfs/image tooling so the runtime can create or fetch runnable filesystems itself.
- Add automated integration tests on Linux for run, stop, logs, memory, and networking.
Built a Docker-like container runtime in Go using Linux namespaces, chroot, cgroup v2 memory controls, bridge networking, and metadata-backed lifecycle commands (run, ps, logs, stop) to explore core container internals end to end.
Built tiny-docker-go, a lightweight container runtime in Go that launches processes in isolated Linux namespaces, applies cgroup memory limits, manages logs and metadata, and connects containers through a custom bridge/veth/NAT network stack. The project is designed as a hands-on deep dive into how container runtimes work under the hood.