TRT
Sign in
System DesignUpdated

Containers Package Processes With Shared-Kernel Isolation

A container is a way to package a process and its dependencies while applying isolation and resource controls.

The Runtime Theory Team5 min read#containers#namespaces#cgroups

A container is a way to package a process and its dependencies while applying isolation and resource controls. The important word is process: there is no virtual machine and no second kernel. Understanding what the kernel actually does on your behalf — and what the image promises — explains most container bugs engineers meet in production.

Launch: what happens at docker run

An image supplies filesystem layers and runtime configuration. At launch, the runtime creates a process and configures it with kernel primitives before execing your entrypoint:

text
containerd/runc, on start:
  1. create namespaces and put the process in them
       CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWUSER
  2. mount the image's merged filesystem (overlayfs) at /
  3. drop capabilities, install seccomp + AppArmor/SELinux profiles
  4. move the process into a cgroup, write memory.max / cpu.max
  5. pivot_root, set hostname, then exec the entrypoint

What you get: a process that sees its own PID 1, its own network interface table, its own mount tree — because the kernel reports those views per process group. What you do not get: a private kernel. Every syscall from every container reaches the same host kernel, which is why a kernel bug is a container escape and why kernel-level security policy is part of container design, not an afterthought.

The three mechanisms, and how they fail

Namespaces scope visibility: what the process can see.

bash
$ ls /proc/1/ns/            # inside the container
cgroup  ipc  net  mnt  pid  user  uts

Control groups (cgroups v2) account and limit consumption:

bash
$ cat /sys/fs/cgroup/memory.max
536870912                      # 512 MiB — hit it and the cgroup OOM killer
                               # kills a process in THIS cgroup only
$ cat /sys/fs/cgroup/cpu.max
20000 100000                   # 20 ms of CPU per 100 ms period = 0.2 core

Capabilities and LSM profiles restrict authority: CAP_NET_RAW, CAP_SYS_ADMIN, and the default seccomp profile (which blocks on the order of 40+ syscalls out of the 300+ Linux exposes) decide what a compromised process can still do.

Each has a characteristic production failure, and they are worth knowing cold:

text
memory.max set too low      → OOMKill, exit 137, no stack trace, at the worst moment
cpu.max = "20000 100000"    → throttling. A job needing 50 ms of CPU must wait across
                              at least 3 quota periods, so a 50 ms task can take
                              ~300 ms wall clock with p99 spikes no profiler in the
                              container explains (the throttling is accounted outside it)
cpu.max unset               → one noisy container starves neighbors on shared cores
pid limit / no limit        → fork bomb takes the whole host

The throttling case is the one that generates the most confused incidents: the service's own CPU metrics look fine, average latency barely moves, and p99 doubles every few minutes. The cause lives in cpu.stat (nr_throttled), not in your application metrics.

What an image guarantees — and what it doesn't

An image is a content-addressed set of read-only filesystem layers plus a manifest of configuration (entrypoint, env, exposed ports, user). It guarantees:

  • the same binaries, libraries, and configuration bits on every machine;
  • fast startup — no boot, only exec, typically 100–500 ms for the runtime plus your process;
  • reproducibility by digest (image@sha256:…).

It does not guarantee:

  • the kernel — the container uses the host's, so a glibc from 2026 can run against a 5.15 kernel and hit a missing syscall or an old io_uring bug;
  • the CPU architecture — an amd64 image on an arm64 node fails with exec format error unless you build a multi-arch manifest;
  • the configuration — volume mounts, sysctls, capabilities, and resource limits are all supplied at run time, which is why "works on my laptop" is often a runtime-config gap rather than an image gap.

This is the conceptual gap the original check question targets: a container is not a machine image. It is a filesystem plus a policy applied to somebody else's kernel.

Why not use a virtual machine?

A VM virtualizes hardware and runs a full guest kernel: stronger isolation boundary (a kernel exploit in the guest does not reach the host), but gigabytes of memory, minutes to boot, and its own patch lifecycle. Containers share the host kernel: gigabytes become megabytes of page cache shared across containers, startup drops to sub-second, and density on a host goes from tens to hundreds of processes — at the cost of a much larger shared attack surface, mitigated by namespaces, capabilities, seccomp, and user namespaces.

Most production systems run both: containers for density and packaging inside a boundary that is already virtualized (VM per node, or gVisor/Kata when a stronger kernel boundary is required for untrusted workloads).

Key takeaways

  • A container is a kernel-configured process. Namespaces scope visibility, cgroups cap consumption, capabilities limit authority — all on the host's single kernel.
  • Learn the three failure signatures: exit 137 (memory.max), p99 spikes with flat CPU (nr_throttled), and exec-format errors (architecture mismatch).
  • An image ships a filesystem, not a machine. No kernel, no CPU arch guarantee, no runtime config — digests make the filesystem reproducible, nothing else.
  • Shared kernel = shared blast radius. Keep the runtime patched, run untrusted code behind a stronger boundary, and never give containers privileged mode as a debugging shortcut.

Kubernetes Documentation: Containers covers the runtime contract as Kubernetes defines it.

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save