What a Container Actually Is
A junior engineer asks you 'what's the difference between a container and a VM?' You say 'containers share the host kernel.' They ask 'what does that actually mean?' and you realize you're not sure. Let's fix that.
Here is the single most important sentence in this entire course: a container is a normal Linux process. That is it. There is no container object in the kernel, no struct container in the source, no special container mode a process runs in. When you docker run nginx, the kernel starts a process the same way it starts every other process, then restricts what that process can see, use, and do.
Everything you think of as "a container" is an illusion assembled from four ordinary Linux features that all predate Docker by years. Docker's insight was not a new kernel primitive. It was packaging those features behind one command and a distributable image format. Once you see the four pieces, containers stop being magic and every container problem you have ever hit (an escape, an OOM kill, a mysterious permission error, a bloated image) becomes a question about one specific piece.
A container is a Linux process with four restrictions applied to it: namespaces control what it can see, cgroups control what it can use, capabilities (plus seccomp, LSMs) control what it can do, and a pivoted root filesystem controls what it runs against. Remove all four and you have a normal process. The kernel has no concept of "container" tying them together; the runtime does.
The four ingredients of the illusion
Every container is these four things applied to a process. The rest of Module 1 takes each one apart in depth; here is the map.
- Namespaces, what it can see. A namespace wraps a global kernel resource so the process inside sees its own isolated instance. A PID namespace makes the process believe it is PID 1 and that no other processes exist. A network namespace gives it its own interfaces and routing table. A mount namespace gives it its own filesystem tree. Namespaces isolate visibility, not resources. (Lesson 1.2)
- cgroups, what it can use. Control groups cap and account for the physical resources a process may consume: memory, CPU time, block IO, PIDs. This is the difference between "the process cannot see other processes" (a namespace) and "the process cannot use more than 512MB of RAM" (a cgroup). (Lesson 1.3)
- Capabilities, what it can do. The kernel splits the historic all-or-nothing power of root into roughly 40 discrete capabilities. A container process usually runs with a small subset, so even a process running as UID 0 cannot load kernel modules or change the system clock unless it holds the specific capability. Seccomp and AppArmor/SELinux narrow this further. (Lessons 1.4 and 1.5)
- The root filesystem, what it runs against. The runtime uses
pivot_root(orchroot) inside a private mount namespace so the process sees the image's filesystem as/, assembled from stacked image layers by a union filesystem like overlayfs. (Module 2)
No single one of these is "the container." The container is the combination, orchestrated by a runtime.
Prove it: a container is just a process
The fastest way to internalize this is to watch a container appear as an ordinary process on the host. Start one and ask Docker for its host PID:
# Start a container in the background
docker run -d --name demo nginx
# Ask the runtime what host PID the container's main process got
docker inspect --format '{{.State.Pid}}' demo
# 48213
That number is a real PID in the host's process table. From the host, it is nothing special:
# The container's process, seen from the host: a normal process
ps -o pid,ppid,comm -p 48213
# PID PPID COMMAND
# 48213 48190 nginx
# Its parent (48190) is the containerd-shim, not Docker. The shim is
# just another host process. Kill the process and the container dies.
Inside the container, that same process believes it is PID 1:
docker exec demo ps -o pid,comm
# PID COMMAND
# 1 nginx
One process, two PIDs: 48213 on the host, 1 inside. That gap is the PID namespace. Now look at what makes the process a "container": its namespace and cgroup membership, both readable from /proc.
# Each of these symlinks points to a namespace the process belongs to.
# Compare them against your own shell's namespaces to see which are shared.
sudo ls -l /proc/48213/ns/
# lrwxrwxrwx net -> 'net:[4026532567]'
# lrwxrwxrwx pid -> 'pid:[4026532565]'
# lrwxrwxrwx mnt -> 'mnt:[4026532563]'
# ... uts, ipc, cgroup, user
# Your shell sits in the host namespaces (different inode numbers)
readlink /proc/self/ns/pid
# pid:[4026531836] <- host PID namespace, different from the container's
# The resource limits are a cgroup, also just a file
cat /proc/48213/cgroup
# 0::/system.slice/docker-<container-id>.scope
There is no /proc/48213/container file, because there is nothing to point it at. "Container" is our word for a process whose ns/ symlinks and cgroup entry have been set up by a runtime. The kernel only knows about the namespaces and the cgroup.
Container vs VM, at the kernel level
This is the distinction the scenario asks about, and now you can answer it mechanically rather than with a slogan. The dividing line is the kernel.
Container vs Virtual Machine at the kernel level
Container
An isolated process on the host kernel
Virtual Machine
A guest OS on virtual hardware
A VM virtualizes the hardware: the hypervisor gives each guest fake CPUs, memory, and devices, and each guest boots its own kernel. The isolation boundary is the virtual hardware, enforced with CPU support (Intel VT-x, AMD-V). A container virtualizes the operating system view: every container calls into the one shared host kernel, and the isolation boundary is the syscall interface plus the namespace/cgroup/capability restrictions layered on it.
That single fact, one shared kernel, explains the entire tradeoff. Containers start in milliseconds and add almost no overhead because there is no second kernel to boot or run. But because every container makes syscalls straight into the host kernel, a kernel vulnerability is reachable from inside a container in a way it is not from inside a VM. This is the mechanical root of "containers are not a security boundary," which Module 4 examines in full, and the reason sandboxed runtimes like gVisor and Kata (Lesson 3.4) exist to put a stronger boundary back.
When someone says "containers are lightweight," the precise reason is that there is no guest kernel. When someone says "VMs are more secure," the precise reason is that the isolation boundary is the hypervisor and virtual hardware rather than the shared kernel's syscall surface. Both statements are about the same single variable: whether a second kernel is in the picture.
Build a container by hand
You do not need Docker to make a container, because a container is not a Docker thing. unshare is a standard util-linux tool that runs a program in new namespaces. This creates a process that thinks it is PID 1 in its own UTS, PID, and mount namespaces, as an unprivileged user:
# -U new user ns, -p new PID ns, -f fork so bash becomes PID 1,
# -m new mount ns, -u new UTS ns, --map-root-user maps you to root inside
unshare -Upfmu --map-root-user --mount-proc bash
# Inside the new "container":
echo $$
# 1 <- this shell is PID 1 in its PID namespace
hostname isolated # UTS namespace: changing hostname does not affect the host
ps aux # mount-proc gave us a private /proc; we see only our own processes
# USER PID ... COMMAND
# root 1 ... bash
# root N ... ps aux
You just built the core of a container with one command and no daemon. Add a cgroup to cap its memory, drop capabilities, and pivot_root into an image's filesystem, and you have reconstructed, by hand, exactly what runc does on every docker run. Lesson 3.2 walks that full sequence in runc.
Common misconceptions
- "A container is a lightweight VM." No. A VM runs a second kernel on virtual hardware; a container is a host process with restrictions. The mental model of "small VM" leads directly to wrong security and performance conclusions.
- "There is a container process/object in the kernel." There is not.
pson the host shows container processes as ordinary processes because that is what they are. The runtime, not the kernel, ties the pieces together. - "Namespaces limit resources." They limit visibility. Isolating what a process can see is a namespace; capping what it can consume is a cgroup. Conflating the two is why people are surprised when a container with a PID namespace but no memory cgroup eats all the host RAM (Lesson 1.3).
- "Running as root inside a container means root on the host." Not by itself. With a user namespace, capabilities dropped, and no dangerous mounts, container-root is heavily defanged. Whether it is dangerous depends on capabilities and configuration (Lesson 1.4), not on the UID alone.
- "Docker is the container." Docker is one runtime and tooling layer. The container is the kernel-level construct; containerd, CRI-O, Podman, and plain runc produce the same thing (Module 3).
Summary
A container is a Linux process the kernel starts normally, then restricts with four independent mechanisms: namespaces (visibility), cgroups (resources), capabilities and syscall filters (privilege), and a pivoted union filesystem (what it runs against). There is no container primitive in the kernel; the runtime assembles the illusion. The difference from a VM is one variable, whether a second kernel exists, and that variable explains both why containers are cheap and why their isolation is weaker. The rest of this module takes each of the four mechanisms apart.
There is no 'container' primitive in the Linux kernel. Explain what a container actually is, mechanically.