How Linux Containers Work: Namespaces, Cgroups and Image Layers
Why a container is really just a Linux process with a clever disguise, and how the kernel pulls it off

"It works on my machine" used to be the end of the conversation. Containers turned it into a solved problem by letting you ship the machine's view of the world along with your app. But a container isn't a tiny virtual computer. It's an ordinary Linux process wearing a very convincing disguise.
This guide explains how Linux containers work from the inside: the kernel features that hide the rest of the system, the controls that stop one app from eating all the memory, and the stacked image layers that make pulling and starting containers so fast. You can try most of it with a few commands.
A container is just a process with a limited view
Docker's own getting-started guide describes containers as isolated processes for each part of your app. That's the whole trick. When you run a container, the kernel starts a normal process, then wraps it in three things:
- Namespaces change what the process can see: its own process list, network, hostname and filesystem.
- Control groups (cgroups) change what the process can use: how much CPU, memory, disk I/O and how many processes.
- A layered root filesystem gives it its own set of files, built from a read-only image plus a thin writable layer.
That's also the key difference from a virtual machine. Docker's documentation points out that each VM runs a complete operating system with its own kernel, while containers on a host all share that host's kernel. No second kernel to boot means containers start in a fraction of a second and pack far more densely onto the same hardware. In the cloud, the two are often combined: a VM runs a container runtime, which runs many containers.
Namespaces: what a container can see
The Linux manual page on namespaces sums them up neatly: a namespace makes a global system resource look like the process has its own private copy, and changes inside stay invisible to everyone outside. Linux currently has eight kinds:
| Namespace | What it isolates | What the container notices |
|---|---|---|
| PID | Process IDs | Its main process is PID 1; host processes are invisible |
| Mount | Mount points | Its own root filesystem and mounts |
| Network | Network devices, stacks and ports | Its own interfaces, IP address and port 80 |
| UTS | Hostname and NIS domain name | Its own hostname |
| IPC | System V IPC and POSIX message queues | Shared memory only with its own processes |
| User | User and group IDs | Can be "root" inside while unprivileged outside |
| Cgroup | The cgroup root directory | Sees its own cgroup as the top of the tree |
| Time | Boot and monotonic clocks | Its own clock offsets (added in Linux 5.6) |
You don't need Docker to see a namespace at work. On a Linux machine with root access, the unshare tool starts a shell in fresh namespaces:
# Start a shell in a new PID namespace with its own /proc
sudo unshare --pid --fork --mount-proc sh
# Inside: the shell thinks it is process 1
ps aux
# PID COMMAND
# 1 sh
# 2 ps aux
# On the host, in another terminal: list namespaces
lsns
Cgroups: what a container can use
Hiding things isn't enough. A container that can't see its neighbors could still hog every CPU core. The kernel's cgroup v2 documentation defines a control group as a way to organize processes into a hierarchy and share system resources along it in a controlled way. Each resource is handled by a controller:
- cpu splits CPU time with weights and hard bandwidth caps, set in
cpu.maxas a quota per period. - memory sets limits and protections.
memory.maxis the hard ceiling. - io controls disk bandwidth and operations per second.
- pids caps how many processes or threads can exist, via
pids.max, which stops a runaway fork loop.
Container tools turn friendly flags into these files for you:
# Cap a container at 256 MB of RAM, 1.5 CPUs and 100 processes
docker run --memory 256m --cpus 1.5 --pids-limit 100 nginx
# From inside any process: which cgroup am I in?
cat /proc/self/cgroup
memory.max, cpu.max or pids.max.Image layers: where a container's files come from
The third ingredient is the filesystem. A container image is a stack of read-only layers. Docker's storage documentation explains that each Dockerfile instruction that changes files adds a layer containing only the differences from the one below, while metadata-only instructions such as LABEL and CMD don't add any.
FROM ubuntu:22.04 # layer: base filesystem
LABEL team="web" # metadata only, no layer
COPY . /app # layer: your files
RUN make /app # layer: build output
RUN rm -r $HOME/.cache # layer: records the deletion
CMD ["python", "/app/app.py"] # metadata only, no layer
When a container starts, the runtime adds one thin writable layer on top. Every file the container creates, edits or deletes lands there, using a copy-on-write approach: a file is copied up from a lower layer only when the container first modifies it. Delete the container and its writable layer goes with it, while the image underneath stays untouched.
This design pays off in three ways:
- Sharing. Ten containers from the same image share one copy of its layers on disk; each adds only its own small writable layer.
- Caching. Images that start from the same base reuse the layers they have in common, so pulls and builds skip what's already there.
- Speed. Starting a container doesn't copy the image. It just stacks a new empty layer on top.
There's one classic gotcha in that Dockerfile. Removing .cache in a later RUN doesn't shrink the image, because the files still exist in the earlier layer. Clean up in the same instruction that creates the mess.
The standards that make containers portable
Containers would be far less useful if every tool invented its own format. In June 2015, Docker, CoreOS and others launched the Open Container Initiative (OCI) under the Linux Foundation, and Docker donated its runtime, runc, as a starting point. The OCI now maintains three specifications: one for the runtime, one for the image format and one for distributing images through registries.
The image spec describes an image with a small JSON manifest. It points to a config object and lists each layer, base layer first, by media type, size and a SHA-256 digest of its contents:
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": { "mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:b5b2…", "size": 7023 },
"layers": [
{ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:9834…", "size": 32654 }
]
}
Naming every blob by the hash of its contents is the same idea that powers Git's object database: identical layers are stored once, and any corruption shows up as a mismatched hash. It's also why pinning an image by digest, not just a tag like latest, guarantees you deploy exactly the bytes you tested.
Containers in practice: what isolation doesn't give you
Because every container shares the host kernel, a kernel bug can in principle affect them all. Isolation is strong, but it isn't the same boundary a VM provides. A few habits help:
- Run as a non-root user inside the container, and use user namespaces where your runtime supports them.
- Set memory, CPU and process limits on every service so one bug can't starve the rest.
- Treat containers as disposable. Keep data in volumes or databases, because the writable layer vanishes with the container.
- Rebuild images regularly to pick up patched base layers, and include image rebuilds in your disaster recovery runbook.
Key takeaways
- A container is a regular Linux process, not a mini virtual machine; all containers on a host share its kernel.
- Namespaces control what a container can see: processes, network, mounts, hostname, users and more.
- Cgroups control what it can use; hitting
memory.maxtriggers the OOM killer inside that container only. - Images are stacks of read-only, content-addressed layers plus one writable layer per container.
- OCI specifications let images built with one tool run on any compliant runtime.
Frequently asked questions
Is a container the same as a virtual machine?
No. A virtual machine runs a full operating system with its own kernel. A container is an isolated process that shares the host's kernel, which makes it lighter and faster to start.
Can Linux containers run on Windows or macOS?
Linux containers need a Linux kernel, so desktop tools on Windows and macOS run them inside a lightweight Linux virtual machine behind the scenes.
Why did my container get OOMKilled?
It reached its cgroup memory limit and the kernel could not reclaim enough memory, so the OOM killer stopped a process inside that container. Raise the limit or reduce the app's memory use.
Why didn't deleting files in my Dockerfile make the image smaller?
Each instruction creates a new layer, and earlier layers are read-only. Files removed in a later step still exist in the layer that added them, so delete them in the same RUN instruction.









Comments
No comments yet — be the first to share your thoughts.
Join the discussion