Namespaces & cgroups by hand
Build a container with no Docker.
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.
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.
$ sudo unshare --pid --mount --uts --net --fork --mount-proc /bin/bashroot@host:/# ps -ef # only our own processes — bash is PID 1UID PID PPID CMDroot 1 0 /bin/bashroot@host:/# hostname box # our own hostname; the real host is unaffectedroot@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.
# 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