CoursesAdvanced Linux internals & toolingKernel from first principles

Namespaces & cgroups by hand

Build a container with no Docker.

Advanced14 min · lesson 11 of 17

The deepest way to understand Linux — and containers — is to build a "container" by hand from the kernel primitives, with no Docker involved. There is no container object in the kernel: a container is just a normal process placed in its own namespaces (which control what it can see) and constrained by cgroups (which control what it can use). Assembling one manually makes both crystal clear and removes all the mystery from the container courses.

The two halves of a "container"
namespaces — what it SEES
pid
own process tree, own PID 1
mnt
own filesystem view
net
own interfaces + ports
uts / ipc / user
hostname, IPC, UID mapping
cgroups — what it USES
cpu / memory / pids
capped share of resources
unshare creates namespaces; a cgroup applies limits. Together = a container, hand-built. Docker just automates this.

Namespaces with unshare

The unshare command runs a process in fresh namespaces of your choosing. Give it its own PID, mount, UTS, and network namespaces and you have isolated a shell: inside, it is PID 1, sees its own process list, has its own hostname, and an empty network. This is exactly what runc does when Docker starts a container — and doing it by hand shows there is no magic, just kernel features you can invoke directly.

terminal
$ sudo unshare --pid --mount --uts --net --fork --mount-proc /bin/bash
root@host:/# ps -ef # only our own processes — bash is PID 1
UID PID PPID CMD
root 1 0 /bin/bash
root@host:/# hostname box # our own hostname; the real host is unaffected
root@host:/# ip addr # an empty network namespace (just lo, down)

cgroups apply the limits

Namespaces isolate the view; cgroups cap the resources. In the cgroup v2 hierarchy you create a group, set limits by writing to its control files, and place a process in it by writing its PID — after which the kernel enforces the ceilings. This is the same mechanism systemd uses for service resource control, exposed raw. Combine an unshared process with a cgroup and you have manually built what a container runtime does: an isolated, resource-limited process.

terminal
# cgroup v2: create a group, cap it, and put a process in it
$ sudo mkdir /sys/fs/cgroup/demo
$ echo "100M" | sudo tee /sys/fs/cgroup/demo/memory.max # 100M memory ceiling
$ echo "50000 100000" | sudo tee /sys/fs/cgroup/demo/cpu.max # 50% of one CPU
$ echo $$ | sudo tee /sys/fs/cgroup/demo/cgroup.procs # move THIS shell into it
# this shell (and its children) is now memory- and CPU-limited by the kernel
Namespaces are isolation, not a security boundary
Building a container by hand also reveals its limits: namespaces and cgroups isolate and constrain, but the process still runs against the shared host kernel, so this is not the strong boundary a VM provides — exactly the point the container-security course makes. A hand-rolled unshare "container" is superb for understanding, and dangerous as isolation for untrusted code, because none of the hardening (capabilities, seccomp, user namespaces) is applied. Understand the primitives here; rely on the hardened runtime, not raw unshare, in production.