Docker & ContainersI Β· FoundationsLinux primitives
The Linux primitives behind a container
What you'll learn
- Name the five kernel primitives that together make a container
- Locate each one on a running container from the host
- Explain which host tools work on containers, and why
- Recognise which primitive is responsible for a given symptom
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-11
The containers-versus-VMs lesson made a claim: a container is a Linux process with kernel features wrapped around it. This lesson makes that claim concrete by taking one running container and pointing at every feature in turn, from the host.
You do not need to master any of them here. Part II of this course takes each apart in detail. What you need by the end of this lesson is the map β five names, what each one does, and where to look β so that later parts have somewhere to attach.
The five primitives
| Primitive | Kernel feature | Governs | Where to look |
|---|---|---|---|
| Namespaces | clone(2) flags | What the process can see | /proc/PID/ns/ |
| cgroups | cgroup v2 hierarchy | What it may use | /sys/fs/cgroup/... |
| Capabilities | capabilities(7) | What it may do as root | /proc/PID/status |
| Union filesystem | OverlayFS | What its root filesystem is | findmnt, docker inspect |
| Syscall filtering | seccomp, AppArmor, SELinux | Which syscalls it may make | /proc/PID/status |
Docker is none of these. Docker is the program that decides which values to use and asks the kernel to apply them. Every one of them predates Docker and is usable without it.
The tour
Start a container and find its process ID on the host. That number is the thread that connects everything below.
$ docker run -d --name web --memory 256m --cpus 0.5 nginx:1.29-alpineb7d2a91f4e6c8a3d05b1c9e7f2a48d6031e5b7c9a0d2f4e68b1c3a5d7e9f0b2c4Illustrative output
$ docker inspect web --format '{{.State.Pid}}'184213Illustrative output
It is an ordinary process
$ ps -o pid,ppid,user,rss,args -p 184213 PID PPID USER RSS COMMAND
184213 184190 root 4212 nginx: master process nginx -g daemon off;Illustrative output
There is nothing container-shaped about that line. ps lists it,
kill can signal it, renice can reprioritise it, strace can
trace it. The hostβs process table is the real process table; the
one inside the container is a filtered view of it.
Namespaces β what it can see
A namespace is a kernel-maintained scope, identified by an inode number. Two processes in the same namespace share that view of the world; two in different namespaces do not.
$ sudo ls -l /proc/184213/ns/lrwxrwxrwx 1 root root 0 Aug 11 09:14 cgroup -> cgroup:[4026532614]
lrwxrwxrwx 1 root root 0 Aug 11 09:14 ipc -> ipc:[4026532552]
lrwxrwxrwx 1 root root 0 Aug 11 09:14 mnt -> mnt:[4026532550]
lrwxrwxrwx 1 root root 0 Aug 11 09:14 net -> net:[4026532617]
lrwxrwxrwx 1 root root 0 Aug 11 09:14 pid -> pid:[4026532553]
lrwxrwxrwx 1 root root 0 Aug 11 09:14 time -> time:[4026531834]
lrwxrwxrwx 1 root root 0 Aug 11 09:14 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Aug 11 09:14 uts -> uts:[4026532551]Illustrative output
Compare with your own shell:
$ readlink /proc/self/ns/pid /proc/self/ns/mnt /proc/self/ns/net /proc/self/ns/userpid:[4026531836]
mnt:[4026531832]
net:[4026531833]
user:[4026531837]Illustrative output
Three of those numbers differ and one β user β is identical. That
single detail is the most important thing on this page: without user
namespaces enabled, root inside the container is root on the
host, the same UID 0, in the same user namespace. Everything that
stops it behaving like root is capabilities and syscall filtering,
not identity.
lsns lists namespaces system-wide, which is the quickest way to see
which processes share which:
$ sudo lsns -t net NS TYPE NPROCS PID USER COMMAND
4026531833 net 412 1 root /sbin/init
4026532617 net 3 184213 root nginx: master process nginx -g daemon off;Illustrative output
cgroups β what it may use
The --memory 256m and --cpus 0.5 from the docker run line above
became kernel settings. Follow the pointer:
$ cat /proc/184213/cgroup0::/system.slice/docker-b7d2a91f4e6c8a3d05b1c9e7f2a48d6031e5b7c9a0d2f4e68b1c3a5d7e9f0b2c4.scopeIllustrative output
$ cd /sys/fs/cgroup/system.slice/docker-b7d2a91f*.scope && grep . memory.max memory.current cpu.max pids.maxmemory.max:268435456
memory.current:5185536
cpu.max:50000 100000
pids.max:maxIllustrative output
268435456 is 256 MiB. 50000 100000 means β50 ms of CPU time per
100 ms periodβ, which is --cpus 0.5. The flags you typed are a
friendly front end to these files, and the files are the truth.
Capabilities β what it may do as root
$ grep CapEff /proc/184213/statusCapEff: 00000000a80425fbIllustrative output
$ capsh --decode=00000000a80425fb0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcapThat is Dockerβs default set: fourteen capabilities out of roughly
forty. The absent ones are the interesting list β CAP_SYS_ADMIN,
CAP_SYS_MODULE, CAP_SYS_PTRACE, CAP_NET_ADMIN β which is why a
container cannot load a kernel module or reconfigure the hostβs
firewall despite running as root.
--privileged replaces that mask with all of them, which is the
whole reason the flag is dangerous.
The root filesystem
$ docker exec web head -1 /proc/mountsoverlay / overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/K3F...,upperdir=/var/lib/docker/overlay2/9a1.../diff,workdir=/var/lib/docker/overlay2/9a1.../work 0 0Illustrative output
(/proc/mounts rather than findmnt, because Alpine-based images do
not carry findmnt.)
An OverlayFS mount composed of the imageβs read-only layers plus one writable layer created for this container. Deleting the container discards the writable layer, which is the mechanical reason container filesystems are ephemeral and volumes exist.
Syscall filtering
$ grep -E '^Seccomp' /proc/184213/statusSeccomp: 2
Seccomp_filters: 1Illustrative output
Mode 2 means a BPF filter is installed β Dockerβs default seccomp
profile, which blocks several dozen syscalls that a normal workload
never issues. A 0 here means the container is running unconfined,
which is what --security-opt seccomp=unconfined and --privileged
both produce.
Why this map is worth carrying
- A container is not reachable by any special tooling.
strace,perf,gdb,ss,lsofandkillall work on the host PID. When Docker tooling is unhelpful, drop to the process. - Symptoms map to primitives. "Killed with no log" is cgroups (memory). "Operation not permitted as root" is capabilities or seccomp. "It cannot see the other container" is namespaces. "The data vanished" is the writable layer.
- Limits are files. Anything
docker runset, you can read back under/sys/fs/cgroupand verify rather than trust. - The kernel is shared. One kernel, one attack surface, one set of tunables. A sysctl is a host-wide decision even when only one container needs it.
Clean up when you are finished:
$ docker rm -f webwebKnowledge check
Knowledge check Β· 4 questions
Q1. A container running as root cannot load a kernel module, despite being UID 0. Which primitive is responsible?
Q2. You set `--memory 256m` on a container. Where can you read back the value the kernel is actually enforcing?
Q3. Which statements about a container process are true when viewed from the host? Select all that apply.
Q4. Without user namespaces enabled, root inside a container is the same UID 0 as root on the host.
Passing score: 75%. Answers are checked in this browser.
Where next
That closes the foundations. Part II takes each primitive on this map and works through it in depth, starting with namespaces.