Sign in
← Linux: isolation
12 min · medium · Lesson 3 of 4

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.

process PID 20431 on host namespaces what it sees cgroups how much it can eat capabilities what it's allowed root own / same kernel, no engine under the container
Four kernel settings layered on top of a regular process. Remove them, and all that's left is a plain binary.

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

Done reading?

Mark the lesson as read to move your track progress forward.

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.

Get access for $49

Tell the agent: use MCP izi, give me lesson linux-isolation-03-container-as-process