The word "container" invites the wrong mental model. It sounds like a lightweight virtual machine — a little sealed box with its own operating system. It is nothing of the sort. There is no VM, no guest OS, no hypervisor anywhere in a container. A container is simply an ordinary Linux process running directly on the host kernel, which has been lied to about what it can see and constrained in what it can use. Two kernel features do the entire job: namespaces and cgroups. Once you see that, Docker stops being magic and becomes obvious.

Namespaces: control what a process can see

Namespaces are the isolation half. As the NGINX engineering blog defines them, namespaces are "a feature of the Linux kernel that partitions kernel resources such that one set of processes sees one set of resources while another set of processes sees a different set of resources." In other words, a namespace gives a process a private, filtered view of the system — it changes what the process can see.

The kernel provides eight namespace types, and a handful of them are what make a process feel like it's alone on its own machine:

  • PID namespace — the process gets its own process-ID tree starting at PID 1. Inside, it can't see or signal any process on the host. That "PID 1" your container runs as is a fiction created here.
  • Mount (mnt) namespace — its own filesystem mount table, so it sees its own root filesystem rather than the host's.
  • Network namespace — its own network stack: interfaces, routing table, ports. Two containers can each bind port 8080 without colliding.
  • UTS namespace — its own hostname.
  • IPC namespace — its own inter-process communication resources.
  • User namespace — maps user and group IDs, so root (UID 0) inside the container can map to an unprivileged user outside it — a major security lever.

Stack these together and a normal process genuinely believes it has its own process tree, network, filesystem, and hostname. Nothing was virtualized; the kernel just filtered its view.

Cgroups: control what a process can use

Namespaces handle visibility, but visibility isn't enough — an isolated process could still hog the whole machine's CPU or memory. That's the other half: control groups (cgroups). Cgroups "limit, account for, and isolate the resource usage (CPU, memory, disk I/O, network, and so on) of a collection of processes."

If you've configured CPU and memory limits on a Kubernetes pod, you've used cgroups without knowing it — they are the exact kernel mechanism that enforces those limits, throttling CPU and OOM-killing on memory. (That's the subject of a companion post on Kubernetes requests vs limits.) Namespaces decide what the process can see; cgroups decide what it can use.

Put it together: a container is just a process

Now the whole thing collapses into something simple. To "run a container," the runtime (Docker, containerd, runc) does roughly this: start a normal process, but place it inside fresh namespaces, attach it to a cgroup with resource limits, and give it a root filesystem assembled from the image's stacked layers. That's it. There is no second kernel booting, no hardware being emulated. The container process makes ordinary system calls to the same host kernel every other process uses.

You can even see the machinery directly with the unshare tool, which creates new namespaces for a command:

bash
# Illustrative: run a shell in fresh PID, mount, and UTS namespaces
sudo unshare --pid --mount --uts --fork bash

# Inside that shell:
#   echo $$        -> it's PID 1
#   ps aux         -> it sees an isolated process tree
#   hostname foo   -> changes hostname without affecting the host
# No VM. No hypervisor. Just a process with a filtered view.

That is, in miniature, everything a container runtime does — plus a root filesystem and cgroup limits.

Why it matters: the security tradeoff

This architecture is why containers start in milliseconds and add almost no overhead: there's no OS to boot and no hardware to virtualize — every container on a host shares that one kernel. But the same fact is the central caveat. Namespaces and cgroups give strong isolation within a single kernel, not the hard boundary a virtual machine's hypervisor provides. A kernel-level vulnerability can, in principle, affect every container on the host, because they all sit on the same kernel.

That's why serious multi-tenant platforms add extra layers: user namespaces to avoid running as real root, and sandboxes like gVisor or lightweight microVMs (e.g. Firecracker) that put a thinner virtualization boundary back underneath untrusted workloads. For trusted workloads, plain namespaces and cgroups are excellent; for running arbitrary untrusted code, treat "shared kernel" as the risk it is.

Key Takeaways

  • A container is not a VM — it's a normal host process with a filtered view of the system and enforced resource limits.
  • Namespaces control what a process can see (PID, mount, network, UTS, IPC, user, and more) — isolation by visibility.
  • Cgroups control what a process can use (CPU, memory, I/O) — the same mechanism behind Kubernetes resource limits.
  • Containers share the host kernel, which is why they're fast and cheap — but isolation is within one kernel, not a hypervisor boundary.
  • For untrusted workloads, add user namespaces or a sandbox/microVM; "shared kernel" is the security tradeoff.

References