Under the hood: namespaces, cgroups and layered filesystems
What Docker does when it starts a container: namespaces hide the rest of the machine, cgroups cap its resources, and an overlay filesystem stacks a writable layer on a shared image.
By the end of this lesson you'll be able to
0 of 5 completeA container is a process
There's no "container" inside the Linux kernel. When you docker run, Docker starts an ordinary
process and wraps it in three kernel features: namespaces (what it can see), cgroups (what it
can use) and an overlay filesystem (its own /, built from image layers). The Linux course's
Containers on Linux lesson builds one by hand; here you'll see exactly what Docker sets up.
$ docker run -d --name cow alpine:3.22 sleep 300 $ docker exec cow ps # inside: the app is PID 1 PID USER TIME COMMAND 1 root 0:00 sleep 300 8 root 0:00 ps $ docker inspect -f '{{.State.Pid}}' cow # the same process, seen from the host 215435 $ ps -o pid,user,cmd -p 215435 PID USER CMD 215435 root sleep 300
docker run -d --name cow alpine:3.22 sleep 300dockerbuild and run containersrunstart a new container-drun in the background--name cowname the containeralpine:3.22the image to startsleep 300the command run inside, instead of the image’s default
docker exec cow psexecrun a command in a running containercowthe containerpslist the services and their state
docker inspect -f '{{.State.Pid}}' cowinspectshow every detail as JSON'{{.State.Pid}}'the container or imagecowthe container or image
ps -o pid,user,cmd -p 215435pslist processes
Same process, two numbers. That's the whole trick, and it's why host tools such as ps, top,
kill and strace work on containers.
Namespaces: what the container can see
Each namespace gives the process its own copy of one part of the system. Compare the namespace IDs on the host and in a container:
$ readlink /proc/self/ns/pid /proc/self/ns/net # on the host pid:[4026532219] net:[4026531840] $ docker run --rm alpine:3.22 sh -c 'readlink /proc/self/ns/pid; readlink /proc/self/ns/net' pid:[4026532234] net:[4026532236]
docker run --rm alpine:3.22 sh -c 'readlink /proc/self/ns/pid; readlink /proc/self/ns/net'dockerbuild and run containersrunstart a new container--rmdelete the container when it exitsalpine:3.22the image to startsh -c 'readlink /proc/self/ns/pid; readlink /proc/self/ns/net'the command run inside, instead of the image’s default
Different numbers mean different namespaces:
| Namespace | The container gets | You notice it as |
|---|---|---|
pid | its own process list | ps shows only its processes, the app is PID 1 |
net | its own interfaces, IP and ports | it has its own localhost; ports must be published (lesson 10) |
mnt | its own mounts | / is the image, not the host |
uts | its own hostname | docker run --hostname shop … hostname prints shop |
ipc, user, cgroup | shared memory, user IDs, cgroup view | rarely, until something needs them |
cgroups: what the container can use
Control groups limit CPU, memory and process counts. The docker run flags write straight into
the container's cgroup files:
$ docker run --rm --memory 128m --cpus 0.5 alpine:3.22 \ sh -c 'cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max' 134217728 50000 100000
docker run --rm --memory 128m --cpus 0.5 alpine:3.22 \dockerbuild and run containersrunstart a new container--rmdelete the container when it exits--memory 128mmemory limit--cpus 0.5CPU limit, in coresalpine:3.22the image to start\the command run inside, instead of the image’s default
| Flag | cgroup file | Means |
|---|---|---|
--memory 128m | memory.max = 134217728 | 128 × 1024 × 1024 bytes; over it, the OOM killer stops a process (exit 137) |
--cpus 0.5 | cpu.max = 50000 100000 | 50 ms of CPU time in every 100 ms: half a core |
--pids-limit 200 | pids.max = 200 | stops fork bombs |
Without limits, one leaking container can use all the host's memory and take every other container
down with it. docker stats shows live usage against these limits.
Layers and copy-on-write
An image is a stack of read-only layers. Starting a container adds one thin writable layer
on top, and the kernel's overlay filesystem merges them into the / the container sees:
$ docker run --rm alpine:3.22 sh -c 'mount | head -1' overlay on / type overlay (rw,relatime,lowerdir=…/snapshots/227/fs:…/snapshots/218/fs,upperdir=…/snapshots/228/fs,workdir=…)
docker run --rm alpine:3.22 sh -c 'mount | head -1'dockerbuild and run containersrunstart a new container--rmdelete the container when it exitsalpine:3.22the image to startsh -c 'mount | head -1'the command run inside, instead of the image’s default
lowerdir is the read-only layers; upperdir is this container's writable layer.
docker diff lists what a container changed compared with its image:
$ docker diff cow C /root A /root/note.txt C /etc A /etc/resolv.conf.bak D /etc/motd
docker diff cowdockerbuild and run containers
Added, Changed, Deleted. Writing a file that exists in the image first copies it up into the writable layer (copy-on-write), then changes the copy. Deleting one adds a "whiteout" marker that hides it. The image itself never changes, which is why a hundred containers can share one image, and why anything a container writes is lost when you remove it.
$ docker image history alpine:3.22 --format '{{.Size}}\t{{.CreatedBy}}' 0B CMD ["/bin/sh"] 8.97MB ADD alpine-minirootfs-3.22.6-x86_64.tar.gz /
docker image history alpine:3.22 --format '{{.Size}}\t{{.CreatedBy}}'dockerbuild and run containersimagemanage images--format '{{.Size}}\t{{.CreatedBy}}'print only these fields (Go template)
Prove a container is just a process
Start an alpine container with memory and CPU limits, then prove from the outside and the inside that it is a host process with its own PID numbering, its own namespaces, the cgroup limits you set, and a writable layer that records every change.
Notes showing host PID vs PID 1, two namespace IDs, the cgroup values and docker diffReveal one safe solution
$ docker run -d --name probe --memory 128m --cpus 0.5 alpine:3.22 sleep 600 $ docker exec probe ps # PID 1 inside $ ps -o pid,cmd -p "$(docker inspect -f '{{.State.Pid}}' probe)" # a normal PID outside $ docker exec probe readlink /proc/self/ns/pid && readlink /proc/self/ns/pid $ docker exec probe sh -c 'cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max' 134217728 50000 100000 $ docker exec probe sh -c 'echo hi > /root/note.txt && rm /etc/motd' $ docker diff probe C /root A /root/note.txt C /etc D /etc/motd $ docker rm -f probe
docker run -d --name probe --memory 128m --cpus 0.5 alpine:3.22 sleep 600dockerbuild and run containersrunstart a new container-drun in the background--name probename the container--memory 128mmemory limit--cpus 0.5CPU limit, in coresalpine:3.22the image to startsleep 600the command run inside, instead of the image’s default
docker exec probe psexecrun a command in a running containerprobethe containerpslist the services and their state
ps -o pid,cmd -p "$(docker inspect -f '{{.State.Pid}}' probe)"pslist processes
docker exec probe readlink /proc/self/ns/pid && readlink /proc/self/ns/pidreadlink /proc/self/ns/pidthe command to run inside it&&run the next command only if this one worked
docker exec probe sh -c 'cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max'sh -c 'cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max'the command to run inside it
docker exec probe sh -c 'echo hi > /root/note.txt && rm /etc/motd'sh -c 'echo hi > /root/note.txt && rm /etc/motd'the command to run inside it
docker rm -f probermdelete a container-fstop it first if running
The two pid:[…] numbers differ, memory.max is exactly 128 × 1024 × 1024, and docker diff lists your
two changes. After docker rm, start a fresh container from the same image: /root/note.txt isn't there
and /etc/motd is back.
Try it yourself
0 of 6 steps doneRun each task in your own account or terminal, then tick it off.
Quick check
Inside a container, ps shows the app as PID 1. What PID does it have on the host?
Official reading for this lesson
Or get every quiz answer right and it completes itself.