FOSS Meet '26 · Container Workshop

What Is a Container, Really? Building One From Scratch With Linux Primitives

It looks like a tiny VM

  • docker run alpine /bin/sh
  • Its own filesystem, network, processes, limits
  • Feels like a whole machine

But it isn't

  • No hypervisor
  • No guest kernel
  • Just one Linux process that's been lied to about the world

A container = 4 lies + 1 trick

  • Filesystem it's allowed to see
  • Network it's allowed to see
  • Resources it's allowed to use
  • Processes it's allowed to see
  • The trick: copy-on-write images

1 · Filesystem

The root it can see, that can't corrupt the original

Copy-on-write with btrfs

  • Carve a disk, format it btrfs, mount it
  • Each container gets a snapshot, not a copy
shell
$ truncate -s 5G btrfs-disk.img$ mkfs.btrfs -f btrfs-disk.img$ mount -o loop btrfs-disk.img ./mnt

Snapshot = Docker layer

  • Unpack Alpine into a subvolume once
  • Snapshot it per container: instant and cheap
shell
$ btrfs subvolume snapshot base ./my-container

Writes hit the snapshot. The base image is never touched.

Copy-on-write, visually

alpine-base subvolume snapshot container A writes container B writes container C writes shared base blocks: free
Unpack the base image once. Each container gets a btrfs snapshot that shares the base's blocks for free — writes land on the snapshot, so the base image is never touched.

2 · Networking

Give the process a private wire to the host

Bridge + veth pair

  • Bridge = a virtual switch on the host
  • veth pair = a virtual cable, a plug on each end
  • One plug into the bridge, the other into the container
internet wlan0 host uplink bridge0 virtual switch (L2) veth pair virtual cable + more ports… netns_ctr eth0 · 10.0.0.2
A veth pair is a virtual cable: one plug sits in the host bridge (a software switch), the other inside the container. The bridge forwards between every container and the host uplink.

Network namespace

  • An isolated network stack: own interfaces, routes, firewall
  • Move the container's plug inside it
shell
$ ip netns add netns_ctr$ ip link set veth_cont netns netns_ctr

Crossing the boundary

host network namespace wlan0 bridge0 veth_host netns_ctr veth_cont ip link set veth_cont netns netns_ctr
A network namespace is a private network stack. The container's end of the veth pair is moved across the boundary, so it sees only its own interfaces, routes and firewall.

3 · Reaching the internet

The container's address is private — someone has to translate

NAT: three iptables rules

  • Masquerade container traffic as the host's
  • Allow container → internet, and the replies back
shell
$ iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE

How NAT rewrites the address

container 10.0.0.2 MASQUERADE src 10.0.0.2 → host wlan0 8.8.8.8 outbound: source address rewritten · replies: rewritten back
The container's 10.0.0.2 is private and unroutable. MASQUERADE rewrites the source to the host's real address on the way out, and restores it on the replies coming back.

4 · Resource limits

Cap the blast radius

cgroups

  • Limit what it can consume, not just what it sees
  • CPU weight + a hard memory cap
shell
$ cgset -r cpu.weight=50 ctr$ cgset -r memory.max=536870912 ctr   # 512 MB

What cgroups cap

container process cpu.weight = 50 ~half of contended CPU memory.max = 512 MB hard cap → OOM
cgroups limit what a process can consume, not just what it can see: a CPU weight that throttles its share, and a hard memory ceiling the kernel enforces.

5 · Process isolation

The heart of the whole thing

unshare + chroot

  • unshare --pid --mount --uts --ipc
  • → fresh process tree, its shell is PID 1
  • chroot → the snapshot becomes /

One process, four cages

cgexec — resource limits ip netns exec — isolated network unshare — PIDs / mounts / uts / ipc chroot — isolated filesystem /bin/sh
Read it from the outside in: each wrapper closes one cage around the next, until /bin/sh runs as PID 1 inside all four.

Read it from the outside in.

You built a container

  • ps shows almost nothing
  • hostname → my-container
  • ping 8.8.8.8 works through the NAT
  • free -m sees the 512 MB cap

It's not magic

  • Namespaces isolate
  • cgroups limit
  • Copy-on-write gives cheap images
  • Everything else is ergonomics and orchestration

The whole model

Filesystem chroot · btrfs snapshot Network veth · bridge · netns · NAT Resources cgroups Processes unshare · namespaces Images copy-on-write
Five independent mechanisms, one process. Namespaces isolate, cgroups limit, copy-on-write makes images cheap — everything else is ergonomics and orchestration.

Debug, don't guess

  • Crash loop, noisy neighbour, DNS fail, weird mount
  • Each maps back to one of these five layers
  • That's the model that holds up under real load