Linux
This is the keystone. Everything from Concepts becomes concrete here, and every tool above decomposes back into it. You'll hand-build the two things that make a container a container — isolation and limits — and then wire container networking from scratch.
sudo. On native Ubuntu you're set. On macOS or Windows, don't use Docker Desktop or WSL2 for these labs — the kernel networking pieces break there. Spin up a real VM: multipass launch --name lab then multipass shell lab.A container is a normal process wearing two costumes: namespaces (what it can see) and cgroups (what it can use). unshare puts on the namespace costume.
Enter a new PID + mount namespace so the process becomes PID 1 and can only see itself.
$ echo $$ # inside the new shellInside, echo $$ prints 1 and ps aux shows only your own processes. You are PID 1 of a private process world — the core of a container.
Reveal solution
$ sudo unshare --pid --fork --mount-proc bash # now inside: $ echo $$ $ ps aux
Namespaces isolate; cgroups limit. cgroup v2 lives in /sys/fs/cgroup. Set a memory ceiling and the kernel enforces it by killing the offender.
Create a cgroup capped at 50 MB, put a shell in it, and try to allocate 200 MB.
$ cat /sys/fs/cgroup/demo/memory.eventsThe allocation is Killed instantly and oom_kill increments. The bouncer threw out your process — but only inside its cgroup, not the whole host.
Reveal solution
In production, systemd-run --scope -p MemoryMax=50M yourcmd is the one-liner form of all this.
$ echo +memory | sudo tee /sys/fs/cgroup/cgroup.subtree_control $ sudo mkdir -p /sys/fs/cgroup/demo $ echo 50M | sudo tee /sys/fs/cgroup/demo/memory.max $ echo $$ | sudo tee /sys/fs/cgroup/demo/cgroup.procs $ python3 -c "x='A'*(200*1024*1024); import time; time.sleep(60)" $ cat /sys/fs/cgroup/demo/memory.events
A container's network stack is a network namespace — a private, nearly empty copy of interfaces and routes.
Create a namespace net1 and look inside.
$ sudo ip netns exec net1 ip link showOnly lo, and it's DOWN. That empty stack is your blank container network.
Reveal solution
$ sudo ip netns add net1Reach engineers who read the man page
Native, contextual, no tracking — this is how the curriculum stays free.
To connect multiple namespaces you need a virtual switch — a bridge. This is precisely what docker0 is. Give it 10.10.0.1 as the gateway.
Create bridge br0, address it, bring it up.
$ ping -c1 10.10.0.10% packet loss — the bridge IP now lives on your host.
Reveal solution
$ sudo ip link add br0 type bridge $ sudo ip addr add 10.10.0.1/24 dev br0 $ sudo ip link set br0 up
A veth pair is a virtual cable: one end in the namespace, the other on the bridge. Exactly the wiring Docker makes per container.
Wire net1 to br0 as 10.10.0.2/24, bring loopback up, and add a default route via the gateway.
$ sudo ip netns exec net1 ping -c1 10.10.0.10% packet loss — the namespace reaches its gateway across the bridge you built.
Reveal solution
$ sudo ip link add veth1 type veth peer name veth1-br $ sudo ip link set veth1 netns net1 $ sudo ip link set veth1-br master br0 $ sudo ip link set veth1-br up $ sudo ip netns exec net1 ip link set lo up $ sudo ip netns exec net1 ip addr add 10.10.0.2/24 dev veth1 $ sudo ip netns exec net1 ip link set veth1 up $ sudo ip netns exec net1 ip route add default via 10.10.0.1
Add a second namespace on the same bridge and they can talk — through a switch you built by hand. You just recreated container-to-container networking.
Create net2 as 10.10.0.3/24 on br0, then ping between the two.
$ sudo ip netns exec net1 ping -c1 10.10.0.30% loss both ways. This is a Docker bridge network, assembled from primitives.
Reveal solution
$ sudo ip netns add net2 $ sudo ip link add veth2 type veth peer name veth2-br $ sudo ip link set veth2 netns net2 $ sudo ip link set veth2-br master br0 $ sudo ip link set veth2-br up $ sudo ip netns exec net2 ip link set lo up $ sudo ip netns exec net2 ip addr add 10.10.0.3/24 dev veth2 $ sudo ip netns exec net2 ip link set veth2 up $ sudo ip netns exec net2 ip route add default via 10.10.0.1
Paste the setup: it builds net3 that should work but can't reach the gateway. Diagnose it.
sudo ip netns add net3
sudo ip link add veth3 type veth peer name veth3-br
sudo ip link set veth3 netns net3
sudo ip netns exec net3 ip link set lo up
sudo ip netns exec net3 ip addr add 10.10.0.4/24 dev veth3
sudo ip netns exec net3 ip link set veth3 up
sudo ip netns exec net3 ip route add default via 10.10.0.1
# (something is missing on the host side...)The namespace side looks complete. Check the host end of the cable — ip link show veth3-br and bridge link.
$ sudo ip netns exec net3 ping -c1 10.10.0.10% loss once fixed.
Reveal solution
The host end was never enslaved to the bridge or brought up — the cable dangled. The most common real container-networking fault: the pod end is fine, the host veth isn't attached.
$ sudo ip link set veth3-br master br0 $ sudo ip link set veth3-br up
Private 10.10.0.0/24 can't reach the internet — no route back. Docker fixes this with a MASQUERADE rule. Same trick.
Enable forwarding and masquerade the subnet, then ping a public IP from net1.
$ sudo ip netns exec net1 ping -c1 8.8.8.80% loss (needs host internet).
Reveal solution
If it still fails, your host FORWARD policy may be DROP: sudo iptables -P FORWARD ACCEPT.
$ sudo sysctl -w net.ipv4.ip_forward=1 $ sudo iptables -t nat -A POSTROUTING -s 10.10.0.0/24 ! -o br0 -j MASQUERADE
Namespaces + cgroups + a rootfs = a container. Bridge + veth + NAT = docker0. You built all of it by hand, so Docker and Kubernetes are now just automation over primitives you understand. Clean up with sudo ip netns del net1 net2 net3; sudo ip link del br0.