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:
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 entrypointWhat 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.
$ ls /proc/1/ns/ # inside the container
cgroup ipc net mnt pid user utsControl groups (cgroups v2) account and limit consumption:
$ 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 coreCapabilities 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:
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 hostThe 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
glibcfrom 2026 can run against a 5.15 kernel and hit a missing syscall or an oldio_uringbug; - the CPU architecture — an
amd64image on anarm64node 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.