https://github.com/martishin/toy-container-runtime
https://github.com/martishin/toy-container-runtime
cgroups chroot devcontainer docker golang linux namespace
Last synced: 3 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/martishin/toy-container-runtime
- Owner: martishin
- Created: 2025-10-30T00:02:38.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2025-11-02T01:46:15.000Z (7 months ago)
- Last Synced: 2026-01-26T04:48:57.612Z (4 months ago)
- Topics: cgroups, chroot, devcontainer, docker, golang, linux, namespace
- Language: Go
- Homepage:
- Size: 15.6 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# toy-container-runtime π
A tiny container runtime written in Go that demonstrates **how containers actually work** - no magic, just Linux
features:
- **Namespaces** (UTS, PID, Mount) β isolate what a process can *see*
- **chroot + rootfs** β give the process its own filesystem view
- **cgroups (pids)** β limit what a process can *use*
- Minimal **mount setup** β `/proc` and a scratch `tmpfs`
## Why containers?
Containers are *just Linux processes* with:
- a **restricted view** (namespaces),
- a **different root filesystem** (chroot/pivot_root with a βrootfsβ),
- and **resource limits** (cgroups).
This makes them **portable** (ship a filesystem), **isolated** (canβt see host stuff), and **efficient** (no full VM per
app). Real runtimes (runc/containerd/Docker) add a ton of safety and orchestration around these same primitives.
## What this project demonstrates
1) **UTS namespace** β custom hostname (`container`).
2) **PID namespace** β your process becomes **PID 1** inside the container.
3) **Mount namespace** β private mount table (no host pollution).
4) **chroot rootfs** β your command runs in a Docker-built filesystem.
5) **/proc + tmpfs** β mounts that userspace tools rely on.
6) **pids cgroup** β best-effort process count limit.
Youβll literally see: a different hostname, PID 1, your own `/proc`, and a private tmpfs.
## How it works
- We build a root filesystem from `Dockerfile` (Alpine + python3 + bash), then `docker export` it to `./rootfs`.
- `mini_container run ...` re-execs itself as `child` with:
- `CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS` (+ `Unshare(CLONE_NEWNS)`)
- set hostname β `container`
- `chroot(rootfs)` and `chdir("/")`
- make `/` a mountpoint, set mount propagation (slave β private)
- mount `/proc` and a `tmpfs` at `/mytemp`
- (best-effort) put the process in a **pids cgroup** to cap forks
- Finally, it `exec`s your command (`/usr/bin/python3 /opt/app/hello.py` or `/bin/bash`).
## Prerequisites
- Linux (or macOS via Dev Containers / Docker Desktop)
- Go 1.24.5
- Docker (to build and export the rootfs)
- In devcontainers: container needs `--privileged`, `SYS_ADMIN`, and the Docker socket mounted
## Running locally
```bash
# Build the runtime
make build
# One-shot: build image -> export rootfs -> run demo app
make run
# Expected: "Hello from container!"
# Interactive shell inside the container
make bash
```
## Verify you are in a container
Inside the container shell:
```bash
hostname # -> container
mount | head # shows /proc and /mytemp tmpfs
ps -ef # PID namespace view (you're PID 1)
cat /proc/self/status | sed -n '1,12p'
```
## Project structure
```
.
βββ Dockerfile # rootfs content (Alpine + python3 + bash)
βββ Makefile
βββ app/hello.py # demo app
βββ cmd/mini_container/main.go # Cobra CLI: build/run/run-rootfs/child
βββ internal/
β βββ build/dockerfile_driver.go # docker build/create/export -> ./rootfs
β βββ container/
β βββ namespaces.go # SysProcAttr (linux)
β βββ fs.go # mount propagation, /proc, tmpfs
β βββ cgroups.go # pids cgroup (v1/v2 best-effort)
β βββ runner.go # Run / RunChild
βββ scripts/build-rootfs.sh
βββ go.mod / go.sum
```