A container as a process
A container as an ordinary process: namespaces + cgroups + capabilities.
Why this matters. When a container "eats all the memory", "can't see a file"
or crashes with Operation not permitted, you look for the cause not in
Docker but in the kernel: in settings the kernel handed to an ordinary
process. This lesson brings together namespaces and cgroups from earlier
lessons, adds capabilities and the image's root filesystem, and shows where
to see each of these settings with your own eyes.
A container is a process
The word "container" sounds like a separate entity, a box with its own machinery inside. In reality it's a regular Linux process that got four things handed to it at startup: limited visibility, resource limits, a cut-down set of rights and its own filesystem root.
The kernel wraps the process in isolation through namespaces, limits its resources through cgroups, and trims its rights through capabilities. There's no separate "virtualization engine" under a container.
You can check this right on the host. Start a container and find it in the regular process list (on Linux, where Docker runs directly on your machine):
docker run -d --name web nginx:1.27.3-alpine
ps aux | grep nginx
root 20431 0.0 0.1 10620 6200 ? Ss 12:03 0:00 nginx: master process nginx -g daemon off;
101 20489 0.0 0.0 11072 2600 ? S 12:03 0:00 nginx: worker process
...
PID 20431 is a number from the host's regular process table. To the host
kernel, the container is no different from any other running binary.
On macOS and Windows (Docker Desktop), containers live inside a small Linux
VM that Docker Desktop starts up. That's why ps aux on the Mac itself won't
show nginx: it's seen by the VM's kernel, not macOS's. The command that works
the same way everywhere asks Docker instead:
docker top web
UID PID PPID C STIME TTY TIME CMD
root 20431 20410 0 12:03 ? 00:00:00 nginx: master process nginx -g daemon off;
101 20489 20431 0 12:03 ? 00:00:00 nginx: worker process
...
docker top shows PIDs the way the Docker host sees them: on Linux that's
your machine, in Docker Desktop that's its VM. The same main process number
comes from docker inspect -f '{{.State.Pid}}' web.
Four layers of isolation
namespaces
A Linux namespace limits what a process sees: its own PIDs, its own network, its own mount points. Inside the container, the main process thinks it's PID 1, while outside it has a number like 20431.
Which namespaces a process sits in shows up as links in /proc/<PID>/ns. On
Linux you can look from the host (needs sudo, the process belongs to
root):
sudo ls -l /proc/20431/ns
...
lrwxrwxrwx 1 root root 0 ... mnt -> 'mnt:[4026532209]'
lrwxrwxrwx 1 root root 0 ... net -> 'net:[4026532211]'
lrwxrwxrwx 1 root root 0 ... pid -> 'pid:[4026532213]'
...
The number in brackets is the namespace's number. Processes in the same
namespace share it, processes in different namespaces don't. The same number
is also visible from inside the container, no sudo needed and on any OS:
docker exec web readlink /proc/1/ns/pid
pid:[4026532213]
cgroups
A cgroup (control group) hangs limits off a process: how much CPU and memory it's allowed to eat. If the processes in a group hit the memory limit and the kernel can't free pages, the OOM killer fires and kills a process inside that group.
The limit set by docker run shows up inside the container as a cgroup
file. Start a container with a limit and read the file:
docker run -d --name web256 -m 256m nginx:1.27.3-alpine
docker exec web256 cat /sys/fs/cgroup/memory.max
268435456
That's 256 × 1024 × 1024 bytes. The file memory.max belongs to cgroup v2,
used by modern distributions and Docker Desktop. On older systems with
cgroup v1, the same limit lives in
/sys/fs/cgroup/memory/memory.limit_in_bytes. To see which version you
have, run docker info --format '{{.CgroupVersion}}'.
The containers from these examples aren't needed anymore, you can remove
them: docker rm -f web web256.
capabilities
capabilities split root's full power into separate rights: change a
file's owner (CAP_CHOWN), listen on a port below 1024
(CAP_NET_BIND_SERVICE), mount filesystems and dozens of other things
(CAP_SYS_ADMIN). A container gets only a part of them.
The kernel writes a process's rights to /proc/<PID>/status:
docker run --rm alpine:3.20.3 grep CapEff /proc/1/status
CapEff: 00000000a80425fb
CapEff is a bitmask of effective capabilities: each bit stands for one
right. Root on the host has every bit the kernel knows about set, on kernels
5.9 and newer that's 000001ffffffffff. A container's mask is shorter:
Docker grants 14 rights by default.
capsh from the libcap2-bin package (Debian, Ubuntu) decodes the mask into
names. It's not in the alpine image, so it's easier to run it on a Linux
host:
capsh --decode=00000000a80425fb
0x00000000a80425fb=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_setfcap
cap_sys_admin isn't in the list, for example, so a container like this
can't mount an arbitrary filesystem from inside.
The set can be changed at startup. --cap-drop removes rights, --cap-add
adds them. A common production practice: drop everything (--cap-drop ALL)
and add back only what the app won't start without. The default rights list
and the flag rules are in Docker's documentation, section
Runtime privilege and Linux capabilities.
Root from the image
A process gets its own / by changing the filesystem root. runc, which
Docker uses to start containers, does this with the pivot_root system call:
it moves the process's root onto the unpacked image. The image itself sits
in layers in an overlay filesystem. overlayfs stacks several directories into
one tree, and writes only go to the top layer, which belongs to the
container.
How this differs from a virtual machine
A virtual machine carries its own kernel and runs on top of a hypervisor (a layer that emulates hardware). A container has no kernel of its own. On Linux the check is simple:
uname -r
docker run --rm alpine:3.20.3 uname -r
Both commands give the same version: the same kernel handles the system call
from inside the container. In Docker Desktop the results differ: the first
command shows the macOS or Windows kernel, the second shows the kernel of
Docker Desktop's Linux VM. Containers share exactly that one, visible in
docker info --format '{{.KernelVersion}}'.
Hence the practical conclusion: a container's isolation boundary is thinner than a virtual machine's. A hole in the kernel hits every container on that kernel at once. VMs are separated by their own kernels.
Common mistakes
"A container's isolation equals a virtual machine's." No. A shared kernel means a smaller boundary. A kernel vulnerability or a misconfigured namespace can let a process escape the container.
"It's a container, so root inside is safe." By default Docker doesn't
enable the user namespace, so root in the container is UID 0 on the host
too. What separates it from full root are trimmed capabilities, a seccomp
profile and namespaces. Flags like --privileged or --cap-add ALL remove
that protection.
If the daemon has userns-remap enabled, the picture is different: UID 0
inside maps to an unprivileged host UID from a set range, say 100000.
Inside, the process sees itself as root, the host sees a regular user. How to
enable this mode is in the documentation
Isolate containers with a user namespace.
"A container won't start with Operation not permitted, so it needs
--privileged." --privileged grants every right and access to host
devices. Usually the app is missing one or two specific rights. The error
message names them, and those are exactly the ones to add with --cap-add.
"ps aux on a Mac doesn't show the container, so it's not a process." In
Docker Desktop, container processes live in the Linux VM. Look through
docker top or docker inspect.
What to remember
A container is a process with isolation, not a separate machine. On the
Docker host it shows up in ps and docker top like any other.
Four layers: namespaces (visibility), cgroups (resources), capabilities (rights), root from the image on top of overlay.
The kernel is shared with the Docker host. That's why the isolation boundary is thinner than a virtual machine's with its own kernel.
Next, Docker ties these mechanisms together, and Kubernetes orchestrates them.
What you'll need for the tasks
docker top <container>anddocker inspect -f '{{.State.Pid}}' <container>: the process's PID as the Docker host sees it.docker exec <container> <command>: read files from inside (/proc/1/ns/...,/sys/fs/cgroup/...,/proc/1/status).readlink /proc/1/ns/pid: the PID namespace number.--cap-dropand--cap-addondocker run,docker logsfor the error text.- Reading the fields
HostConfig.CapAddandHostConfig.CapDropindocker inspect.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson linux-isolation-03-container-as-process
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress