Why containers
How a container differs from a VM and from emulation. Namespaces and cgroups in short, and what actually runs under the hood in Docker.
In this lesson we'll look at how a container differs from a virtual machine
and from emulation, what Linux kernel mechanisms create it, and what
actually happens when you type docker run. In the labs you'll check this
with your own hands: you'll see that containers from different
distributions run on one kernel, and you'll find a container the kernel
killed for exceeding its memory limit.
What problem containers solve
The old scenario: "works on my machine, doesn't work on yours". One developer installs one version of a dependency, another installs a different one, and production runs on some old Ubuntu altogether. Setting all this up by hand takes forever, and the environments still drift apart.
A container is a packaged application together with its file system, dependencies and start command. On any Linux machine this package deploys the same way, so production and dev stop drifting apart.
Under the hood a container is an ordinary Linux kernel process with its own namespaces and cgroup limits. There's no separate technology: the kernel isolates the process from the rest of the system.
VM, container and emulation
Three similar words that mean different things.
| What | What runs | Own kernel | Overhead | CPU architecture |
|---|---|---|---|---|
| VM (KVM, VMware) | a full guest OS | yes | gigabytes, seconds to start | same as the host |
| Container | a process on the host kernel | no | megabytes, milliseconds | same as the host |
| Emulation (QEMU) | a guest OS or a program of another architecture | yes, if it's an OS | very expensive on CPU | can be different (arm on x86) |
The key difference between a container and a VM: a container has no kernel of its own. When you run Postgres in a container on a Linux host, postgres stays an ordinary process on the host kernel. The kernel just shows it a trimmed-down view of the system, where there's almost no one else around.
Emulation works differently. QEMU can run code for a foreign architecture (ARM on x86, say), but it translates every instruction and pays for that in speed. When Docker on a Mac with Apple Silicon runs an amd64 image, emulation kicks in: QEMU, or Rosetta if it's enabled in Docker Desktop settings. Such a container runs noticeably slower than a native one. How much slower depends on the workload.
Containers on macOS and Windows
A container needs a Linux kernel. macOS and Windows don't have one, so Docker Desktop brings up a small Linux VM, and all containers run inside it. On Windows, WSL2 usually plays that role.
This is easy to check. docker info shows the kernel the containers live
on:
docker info --format '{{.OperatingSystem}} {{.KernelVersion}}'
On a Mac with Docker Desktop the answer will look something like this:
Docker Desktop 5.15.49-linuxkit
The linuxkit kernel belongs to the Docker Desktop VM, not to macOS. On a
Linux host you'll see the host's own kernel there, on WSL2 the WSL kernel.
"Host kernel" in this lesson means the kernel of the machine where Docker
Engine runs.
Namespaces: what gets isolated
When Docker starts a container, the kernel creates new namespaces for the process: separate namespaces instead of ones shared with the host. The main ones:
- PID: its own process counter. Inside the container the main process gets PID 1, as if it were init. On the host the same process has an ordinary number, say 14000.
- NET: its own network stack, meaning interfaces, IP, the routing
table, iptables rules. The container sees its own
eth0, not the host's interfaces. - MNT: its own tree of mount points. Its root becomes the file system assembled from the image's layers, not the host's root.
- UTS: its own hostname. That's why
hostnameinside a container by default prints the first 12 characters of the container ID, not the machine's name. - IPC: its own inter-process communication objects (shared memory, message queues).
There's also the user namespace (its own uid and gid mapping) and the
cgroup namespace. Docker doesn't enable user by default; it needs
separate configuration (userns-remap). You can skip them for now.
You can see a process's namespaces in /proc/self/ns:
docker run --rm alpine:3.20.3 ls /proc/self/ns
cgroup
ipc
mnt
net
pid
pid_for_children
time
time_for_children
user
uts
Namespaces are created by flags to the clone() and unshare() system
calls. In the narrowest sense, "creating a container" means "starting a
process with the right flags". On Linux you can do this without Docker,
with the unshare command.
cgroups: resource limits
Namespaces isolate what a process sees. cgroups (control groups) limit how much it can take: CPU, memory, disk IO, number of processes.
When you run docker run --memory=512m --cpus=1, Docker creates a cgroup
for the container and puts its processes into it. If the processes try to
take more memory than allowed, the kernel's OOM-killer kills the process
inside that cgroup, and leaves its neighbors alone.
You can see the limit from inside the container too: it's an ordinary cgroup v2 file:
docker run --rm --memory 32m alpine:3.20.3 cat /sys/fs/cgroup/memory.max
33554432
33554432 bytes is exactly 32 MiB. A container killed for memory stops with
exit code 137 (128 + 9, signal SIGKILL). docker kill gives the same code.
The .State.OOMKilled field in docker inspect tells the two apart: after
a memory kill it's true.
Without cgroups, one greedy container could eat all of the host's memory. With cgroups, the problem stays inside the container.
Image layers and copy-on-write
A container image consists of read-only layers. Every Dockerfile
instruction that changes files (COPY, RUN and others) adds a new layer:
an archive of the file system difference.
When you run a container, one writable layer sits on top of the stack of read-only layers. Every change the process makes inside the container is written there. The base layers don't change, and every container started from that image shares them.
This is called copy-on-write. As long as no one touches a file, it stays in the base layer. As soon as a process changes it, a copy of the file rises into the top layer, and further changes go there.
We'll cover this in detail in the lesson about images. For now, one thing matters: the image is immutable, and the container is mutable on top of it.
What starts a container
When you type docker run, a chain of calls runs under the hood, and each
link does its own part of the work:
docker(CLI): a thin wrapper, sends an HTTP request todockerdover a socket.dockerd: the Docker daemon. Holds the list of images and containers, talks to the registry, sets up network and volumes, hands off the start tocontainerd.containerd: manages the container lifecycle (create, start, stop, catch the exit code). For each container it brings up a smallcontainerd-shimprocess, which keeps the container alive even if containerd itself restarts.runc: the OCI runtime. Takes the container's file system andconfig.json, creates the namespaces and cgroup, and starts the process. After starting it, runc exits.- Kernel: only it does the real isolation. Everything above it isn't
required for isolation. You can build a container by hand with
unshare, it's just inconvenient.
Docker isn't the only way to run containers. Podman, containerd directly, kubelet in Kubernetes all end up at an OCI runtime (runc or, say, crun) and at the same kernel mechanisms.
When a container doesn't fit
A container doesn't always fit. Cases where a VM is better:
- You need a different kernel. You can't run a Windows application in a Linux container. Only in a Windows container on a Windows host, or in a VM.
- Strong isolation. Containers share one kernel. If you're running someone else's untrusted code (multi-tenant SaaS, sandboxes), a kernel vulnerability means an escape from the container to the host. A VM boundary is stronger.
- Old software that only installs into a full OS. Sometimes it's easier to spin up a VM than to repackage it into a Dockerfile.
For web applications, backends, databases, queues and CI, a container fits in the vast majority of cases.
Common mistakes
"A container is a lightweight VM". No: a VM has its own kernel, a container doesn't. That's why a container starts in a fraction of a second, but its isolation is also thinner.
"On a Mac the container runs on macOS". No: on macOS and Windows,
containers live inside the Docker Desktop Linux VM. uname -r inside the
container will show that VM's kernel.
"The alpine image means an alpine kernel". An image brings only files:
libraries, utilities, /etc/os-release. There's always one kernel, the
one Docker Engine runs on.
"137 means it ran out of memory". Code 137 only means SIGKILL. Both
the OOM-killer and docker kill give it. Check .State.OOMKilled.
"Docker = containers". Docker is just a convenient wrapper over namespaces, cgroups and OverlayFS. Swap Docker for Podman, and the kernel won't notice the difference.
What you'll need for the tasks
docker exec <name> <command>: run a command inside a running container, for exampledocker exec <name> uname -rordocker exec <name> psdocker ps -a: all containers, including stopped ones, with theSTATUScolumn showing the exit codedocker inspect -f '{{.State.OOMKilled}}' <name>anddocker inspect -f '{{.HostConfig.Memory}}' <name>(memory limit in bytes)docker info --format '{{.KernelVersion}}'
We'll go through docker run flags in detail in the next lesson, where
we'll start our first container. Exactly how the kernel builds isolation
(namespaces, cgroups, capabilities) is covered in the "Linux: isolation"
track.
Interview-style questions
Interview questions on this topic: 2
Included with practice access, $49 one-time. Get access
Answer out loud or in writing, then reveal the answer and honestly mark whether you knew it.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson docker-basics-01-why-containers
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress