Why start with what's under the hood?
People often describe a container as "a lightweight VM". That picture gets you through a demo, and it leads you wrong later: you'll be surprised that a container can't run a Windows kernel on a Linux host, that killing PID 1 stops the whole container, and that a memory limit can get your app killed without warning. So before you write a Dockerfile, it's worth ten minutes on the real model.
A container is an ordinary Linux process. It runs on the host's kernel, just like your editor or your shell. The difference is that the kernel puts two kinds of fence around it:
- Namespaces control what the process can see.
- cgroups (control groups) control what the process can use.
That's the whole trick. There's no second operating system and nothing to boot.
Namespaces: what a process can see
A namespace gives a process its own private view of one part of the system. Linux has eight kinds:
| Namespace | What the container gets its own copy of |
|---|---|
| PID | Process IDs. Its first process is PID 1 |
| Mount | The filesystem tree it sees as / |
| Network | Network interfaces, IP addresses and ports |
| UTS | Hostname |
| IPC | Shared memory and message queues |
| User | User and group IDs |
| Cgroup | Its view of the cgroup tree |
| Time | Boot and monotonic clocks |
An analogy: think of an open-plan office where each team works behind frosted-glass partitions. Everyone shares the same building, power and air conditioning (the kernel), but each team only sees its own desks and whiteboards (processes, files, network).
cgroups: what a process can use
Namespaces hide things, but they don't stop a process from using all the CPU or memory. That's the job of cgroups, a kernel feature that groups processes together so their CPU, memory and I/O can be measured and limited. The office analogy still works: cgroups are the meter on each team's power supply, plus a breaker that trips if they draw too much.
Gotcha: if a container goes over its cgroup memory limit, the kernel's out-of-memory killer ends the process. From outside, your app just looks as if it vanished. You'll set these limits yourself in a later level.
Containers vs virtual machines
A virtual machine emulates hardware. A hypervisor runs a complete guest operating system, with its own kernel, on top of that virtual hardware. A container skips all of that and shares the host's kernel.
| Container | Virtual machine | |
|---|---|---|
| Kernel | Shares the host's | Has its own guest kernel |
| Start-up | About a second or less (it's starting a process) | Tens of seconds or more (boots an OS) |
| Size | Megabytes (just the app and its libraries) | Gigabytes (a whole OS) |
| Isolation | Process-level, enforced by one shared kernel | Hardware-level, enforced by the hypervisor |
| Runs a different OS kernel? | No | Yes |
Which should you pick? Use containers to package and ship applications: web services, workers and build tools, where you want quick starts and dense packing. Use a VM when you need a different kernel or OS (say, Windows on a Linux server), or stronger isolation for code you don't trust, because a kernel bug can let a process escape a container in a way a hypervisor would stop. In real systems you'll often have both: cloud container services usually run your containers inside VMs.
Trade-off worth knowing: Docker Desktop on Windows and macOS runs Linux containers inside a small Linux VM, using WSL 2 on Windows. Your containers share that VM's kernel, not the Windows or macOS kernel. You'll see this in the exercise below.
The four words you'll use every day
- Image: a read-only template containing a filesystem and settings: your app, its libraries and the command to run. Think of it as a class.
- Container: a running (or stopped) instance of an image, the fenced-off process. Think of it as an object. One image can run as many containers.
- Registry: a server that stores and hands out images. Docker Hub is the default public one.
- Container runtime: the software that actually sets up the namespaces and cgroups and starts the process. In Docker, the
dockerCLI talks to the Docker daemon, which hands the work to containerd, which uses runc (the reference runtime of the Open Container Initiative) to create the container.
The flow goes like this: docker run → the daemon checks for the image locally → if it's missing, pulls it from a registry → the runtime creates namespaces and cgroups → your process starts as a container.
Hands-on: a container next to a host process
If Docker isn't installed yet, read along now and run this after Level 2, which covers installation.
# 1. On the HOST: count how many processes are running (Linux/macOS/WSL shell)
ps aux | wc -l
# typical output: 300 or more. Your machine runs a lot of processes
# 2. In a CONTAINER: list every process it can see
# --rm deletes the container when it exits; alpine is a ~few-MB Linux image
docker run --rm alpine ps
Expected output (the first run also prints download progress while it pulls alpine):
PID USER TIME COMMAND
1 root 0:00 ps
That one line is the PID namespace at work. The container sees one process, its own, as PID 1, while the host is running hundreds.
# 3. Compare kernels
uname -r # on the host (Linux) or inside WSL
docker run --rm alpine uname -r
On a Linux host, both commands print the same kernel version, which shows the container has no kernel of its own. On Docker Desktop for Windows you'll see something like ...-microsoft-standard-WSL2, the kernel of the Linux VM that Docker Desktop runs, not Windows. Alpine has no kernel of its own: it's a filesystem running on someone else's kernel.
# 4. Bonus: the UTS namespace gives the container its own hostname
hostname # your machine's name
docker run --rm alpine hostname # a random 12-character ID like 3f9c2a7b1e04
Check your result: you've succeeded if the container's ps shows just one or two processes with PID 1 at the top, and its uname -r matches the host's (Linux) or shows a Linux VM kernel (Desktop). Run step 2 again and see how much faster it is: the image is cached, so all that's left is starting a process.
Key takeaways
- A container is a normal process on the host kernel, fenced off by namespaces (what it sees) and cgroups (what it uses).
- It starts in about a second because nothing boots. A VM has to boot a whole guest OS.
- Containers share the kernel: they're small and fast, but you can't run a different kernel and the isolation is weaker than a VM's.
- Image = template, container = running instance, registry = image store, runtime = what creates the container (containerd + runc in Docker).
- On Windows and macOS, Docker Desktop runs your containers inside a Linux VM.