Module 00 · Foundations
What a Container Actually Is
Meet Sam, a developer about to ship their first containerized app. If Sam only memorises commands, Sam will fall down on the why, the very thing that separates juniors from professionals. This module builds the mental model first: what problem containers solve, what actually makes one isolated, and how Docker's pieces fit together. No fluff.
The problem containers solve
Sam's app runs perfectly on Sam's laptop. Then a teammate pulls it and it dies on import. "It works on my machine." Classic. The app needs a specific Python version, certain system libraries, env vars, a particular Linux distro. Move it to another machine and any mismatch breaks it.
Two historical fixes came before containers:
Install directly on the server
Fragile and unrepeatable. Apps conflict (App A needs OpenSSL 1.1, App B needs 3.0, and they share one host).
Virtual Machines
Each app gets a full guest OS. Strong isolation, but heavy: gigabytes of RAM/disk per VM, slow to boot.
Containers are the third fix, and the one that ends Sam's "works on my machine" headache: package the app and its dependencies into one portable artifact (an image), and run it as an isolated process on a shared kernel. Lightweight like a process, portable like a VM.
Containers vs Virtual Machines
This comparison is the bedrock. A VM stack carries a full guest OS per app; a container stack shares one host kernel and stacks only the app and its deps on top.
VIRTUAL MACHINES CONTAINERS
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ App │ │ App │ │ App │ │ App │ │ App │ │ App │
│ Bins │ │ Bins │ │ Bins │ │ Bins │ │ Bins │ │ Bins │
│Guest │ │Guest │ │Guest │ └──────┘ └──────┘ └──────┘
│ OS │ │ OS │ │ OS │ ┌──────────────────────────┐
└──────┘ └──────┘ └──────┘ │ Container Engine │
┌──────────────────────────┐ ├──────────────────────────┤
│ Hypervisor │ │ Host OS (kernel) │
├──────────────────────────┤ ├──────────────────────────┤
│ Host OS │ │ Hardware │
├──────────────────────────┤ └──────────────────────────┘
│ Hardware │
└──────────────────────────┘| Virtual Machine | Container | |
|---|---|---|
| Isolation unit | Full guest OS + virtual hardware | Isolated process on the host kernel |
| Kernel | Its own | Shares the host kernel |
| Size | GBs | MBs |
| Startup | Seconds–minutes | Milliseconds–seconds |
| Overhead | High (full OS per VM) | Low (just the app + deps) |
| Isolation strength | Stronger (hardware-virtualized) | Weaker (kernel-level), relevant to security |
| Use case | Run different OSes, strong tenant isolation | Package/ship/scale apps, microservices |
A VM virtualizes hardware and runs a full OS; a container virtualizes the operating system and shares the host kernel.
Because containers share the host kernel, a Linux container needs a Linux kernel. On a Linux machine it runs natively. On macOS/Windows, Docker runs a lightweight Linux VM under the hood and your containers run inside that. So "containers don't need a VM" is only true on Linux.
What actually makes a container isolated
Sam pictured a container as some magic box. It isn't. A container is not a special object. It's a normal Linux process that the kernel has been told to restrict and isolate using three kernel features. This is the heart of the foundations.
Namespaces
What the process can SEE. They partition kernel resources so a process gets its own isolated view.
cgroups
What the process can USE. They limit and account for CPU, memory, I/O, and process count.
Union/overlay FS
How image layers become a single writable root via copy-on-write.
a) Namespaces: "what the process can SEE"
Namespaces partition kernel resources so a process sees its own isolated view. The key ones:
| Namespace | Isolates |
|---|---|
| PID | Its own process tree. The main process is PID 1; it cannot see host processes. |
| NET | Its own network interfaces, IP, routing table, ports. |
| MNT | Its own filesystem mount points (its own root /). |
| UTS | Its own hostname. |
| IPC | Its own inter-process communication (shared memory, semaphores). |
| USER | Maps user IDs; lets a process be "root" inside the container but unprivileged on the host (the basis of rootless/hardening, Module 07). |
MNT, UTS, IPC, NET, PID, USER → "Make Unix Isolation Now, Please User."
b) cgroups (control groups): "what the process can USE"
cgroups limit and account resource usage: CPU, memory, disk I/O, network bandwidth, number of
processes (PIDs). This is how docker run --memory=256m works, and it maps directly to
Kubernetes resources.requests/limits later.
Namespaces isolate (visibility); cgroups limit (consumption).
c) Union/overlay filesystem: "how the image becomes a writable root"
An image is a stack of read-only layers. When you start a container, the engine adds a thin
writable layer on top (copy-on-write). Your storage driver overlayfs merges them
into a single view. Delete the container and the writable layer is gone; the underlying image layers are
untouched and reused by other containers. (Layers and caching = Module 02; storage persistence = Module 03.)
overlayfs merges read-only image layers + one writable layer into a single root /.
A host process, isolated by namespaces, resource-bounded by cgroups, running on a copy-on-write union filesystem assembled from image layers.
Image vs container: do not confuse these
Image = blueprint
The read-only template. Built from a Dockerfile. Immutable. Stored as layers.
Container = instance
A running (or stopped) instance of an image, with its own writable layer and isolated namespaces.
Image is the class, container is the object. Or: image is the recipe, container is the cooked dish. Sam can run many containers from one image, which is exactly what happens when the app scales out.
Docker's architecture: client / daemon / registry
When Sam types docker run, several pieces move at once. Docker is not one program.
Knowing the split (and being able to narrate it) is what signals you understand the stack.
docker CLI ──REST API──► dockerd (daemon) ──pull/push──► Registry (e.g. Docker Hub)
(client) │ builds images
│ runs containers (via containerd → runc)
▼
Linux kernel (namespaces, cgroups)| Component | Role |
|---|---|
Docker client docker | What you type. Sends commands over a REST API (local socket /var/run/docker.sock by default). |
Docker daemon dockerd | The long-running server. Builds images, runs/manages containers, networks, volumes. Client and daemon can be on different machines. |
| containerd | High-level container runtime the daemon delegates to (image transfer, container lifecycle). Also what Kubernetes uses directly. |
| runc | The low-level runtime that talks to the kernel to create the namespaces/cgroups and spawn the process. OCI-compliant. |
| Registry | Stores and distributes images (Docker Hub, GHCR, ECR, a private registry, covered in Module 06). |
Trace it: client → daemon → check local image, else pull from registry → containerd → runc → create namespaces/cgroups → start process. Being able to narrate that signals you understand the whole stack, not just the CLI.
OCI: the standards
OCI (Open Container Initiative) defines open specs for the image format and the runtime. This is why images are portable across Docker, Podman, Kubernetes, and more. "Docker image" is colloquial; the precise term is "OCI image." Knowing this signals maturity.
Where this is heading
Every concept here reappears later. As Sam moves toward Kubernetes, keep asking "how does this reappear there?" That habit is the senior mindset.
| Foundation concept | Where it reappears |
|---|---|
| cgroups limits | Kubernetes requests/limits and the scheduler. |
| NET namespace + bridge | Kubernetes pod networking and CNI plugins. |
| daemon / containerd / runc split | Why Kubernetes dropped the Docker shim and talks to containerd directly. |
| Declarative image build | Declarative deployment manifests. |
Hands-on: prove isolation yourself
Sam doesn't take any of this on faith, and neither should you. The point of this exercise is to see namespaces and cgroups with your own eyes. Run each line and predict what it will show before you hit enter.
# 1. Start a long-lived container in the background
docker run -d --name demo nginx:1.27
# 2. PID namespace: inside, nginx is PID 1 and sees no host processes
docker exec demo sh -c 'ps -ef'
PID CMD
1 nginx: master process ... <- PID 1 inside the container
# 3. Same process, seen from the HOST, a normal process with a big PID
ps -ef | grep nginx # it's just a host process, not PID 1 here
# 4. UTS namespace: the container has its own hostname
docker exec demo hostname
# 5. cgroups: cap memory and watch the limit be enforced
docker run --rm --memory=256m alpine sh -c 'cat /sys/fs/cgroup/memory.max'
# 6. Clean up
docker rm -f demo- You saw the container's main process as PID 1 inside, but a normal host process from outside.
- You confirmed the container has its own hostname (UTS namespace).
- You saw a memory limit enforced by cgroups.
- You can recite the precise definition: a host process, isolated by namespaces, bounded by cgroups, on a copy-on-write union filesystem.
Can you cleanly answer: VM vs container? Namespaces vs cgroups? Image vs container? What happens on
docker run nginx? If any of those feel shaky, re-read sections 2, 3, and 5 before
Module 01.