Week 1: Containers vs. Virtual Machines & Docker Architecture

Before you write a single Dockerfile, you need a real mental model of what a container is — because "a lightweight VM" is a comforting lie that breaks the moment you try to debug one. This week builds that model from the ground up: the two Linux kernel features containers are actually made of, how the Docker Engine turns those primitives into a friendly CLI, and running your first container so the theory has something to point at.

Module 1 of 17 Week 1 of 18 ~3 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Explain why a container is a process, not a mini VM, and what isolation it actually gets
  • Describe the Docker Engine's daemon/CLI/containerd/runc pipeline
  • Install Docker and run, inspect, and clean up your first containers confidently

1. Virtual Machines vs. Containers

A virtual machine virtualizes hardware: a hypervisor gives each VM its own virtual CPU, memory and disk, and each VM runs a full guest operating system — its own kernel, its own init system, its own everything. A container virtualizes nothing at the hardware level. It's a regular process on the host, running the host's kernel, that's been given the illusion of its own filesystem, network stack and process tree.

what's actually running, layer by layer
VIRTUAL MACHINES                        CONTAINERS
+-----------------------------+          +-----------------------------+
| App A     | App B            |          | App A     | App B            |
| Bins/Libs | Bins/Libs         |          | Bins/Libs | Bins/Libs         |
| Guest OS  | Guest OS          |          +-----------------------------+
+-----------------------------+          |     Docker Engine (shared)   |
|          Hypervisor          |          +-----------------------------+
+-----------------------------+          |        Host OS Kernel        |
|            Host OS           |          +-----------------------------+
+-----------------------------+          |          Physical Server      |
|         Physical Server      |          +-----------------------------+
+-----------------------------+

That single difference — sharing the host kernel instead of booting a new one — is why containers start in milliseconds instead of minutes, and why a container image is megabytes instead of gigabytes. It's also why the isolation is weaker: a kernel vulnerability can, in principle, be exploited to escape a container in a way that's structurally impossible against a VM's hardware-virtualized boundary.

"Lightweight VM" is a useful lie, briefly

It's fine as a one-sentence intuition for someone who's never seen a container. But the moment you're debugging why a process inside a container can see host processes, or why two containers on the same host can't reach each other, you need the real model: isolated Linux process, not a virtualized machine.

2. The Linux Primitives: Namespaces & cgroups

Containers aren't a Docker invention — Docker is a friendly interface over two Linux kernel features that have existed for years: namespaces, which control what a process can see, and cgroups (control groups), which control what a process can use.

namespaces — isolate what a process sees
PID namespace      # its own process tree — PID 1 inside, invisible to other containers
NET namespace      # its own network interfaces, IP address, routing table
MNT namespace      # its own filesystem mount points — its own view of "/"
UTS namespace      # its own hostname
IPC namespace      # its own inter-process communication (shared memory, semaphores)
USER namespace     # its own UID/GID mapping (root inside != root outside, if configured)
cgroups — limit what a process can use
# Every container is placed in a cgroup that caps its resource usage:
cpu     # how much CPU time the container's processes can consume
memory  # a hard memory ceiling — exceed it and the kernel OOM-kills the container
pids    # how many processes/threads it can fork (stops fork bombs from taking the host down)
io      # disk read/write throughput limits

Put together: namespaces make a container think it's alone on a machine, and cgroups make sure it can't hog the machine even if it tries. Every docker run flag you'll learn — --memory, --cpus, --network — is really just a friendly wrapper around configuring one of these two primitives.

You can see this without Docker

Tools like unshare and chroot let you build a crude container by hand using just these Linux primitives. It's a great weekend rabbit hole if you want the "there's no magic" moment — Docker's real contribution was making this usable, not inventing it.

3. The Docker Engine Architecture

When you type docker run nginx, four components hand the request down the chain, each doing one job:

the pipeline
docker CLI  --(REST API over a Unix socket)-->  dockerd (the daemon)
                                                      |
                                                      v
                                                 containerd  (manages container lifecycle)
                                                      |
                                                      v
                                                    runc  (actually creates the namespaces/cgroups
                                                           and starts the container process)

docker CLI is just a client — it doesn't run containers itself, it sends requests to dockerd, the background daemon. dockerd handles image builds, networks and volumes, and delegates the actual "start/stop/ monitor this container" work to containerd, a smaller, more focused daemon shared by other tools (Kubernetes itself talks to containerd directly, without Docker in the loop, in most modern clusters). runc is the lowest layer — a tiny CLI tool, following the OCI (Open Container Initiative) spec, that does the actual namespace/cgroup setup and execs your process into it.

why this layering matters
# This is why "Docker containers" and "Kubernetes containers" are the same thing
# under the hood — both ultimately run through an OCI-compliant runtime (often
# containerd + runc). Kubernetes doesn't need Docker installed at all; it just
# needs something that speaks the Container Runtime Interface (CRI).
Looking ahead to Kubernetes

Every image you build with docker build in the next few weeks is OCI-compliant, which is exactly why it'll run unmodified on a Kubernetes cluster starting Week 7 — Kubernetes doesn't care what built the image, only that it follows the spec.

4. Installing Docker & Running Your First Container

Install Docker Desktop (Mac/Windows) or Docker Engine (Linux) from docker.com, then verify it's working:

sanity check
docker --version
docker info          # confirms the daemon is running and reachable
docker run hello-world   # pulls a tiny image and runs it end-to-end

That last command is doing more than it looks like: pulling an image from Docker Hub (if you don't have it locally), creating a container from it, starting it, running its command, and printing its output. Let's do it more deliberately with a real, long-running container:

a real container, step by step
docker run -d --name my-nginx -p 8080:80 nginx
# -d          detached — runs in the background, returns your terminal immediately
# --name      a human-readable name instead of a random one
# -p 8080:80  publish container port 80 to host port 8080

docker ps                     # see it running
docker logs my-nginx          # see its stdout/stderr
docker exec -it my-nginx bash # get an interactive shell INSIDE the container
docker stop my-nginx          # send SIGTERM, then SIGKILL after a grace period
docker rm my-nginx            # remove the stopped container (doesn't remove the image)

Open localhost:8080 in a browser while it's running — you're talking to a process that, from the outside, looks exactly like a tiny, disposable Linux server, and from the inside genuinely believes it's the only thing running on its machine.

Containers vs. images — get this straight now

An image is a read-only template (the recipe). A container is a running (or stopped) instance of that image (the dish you made from it). You can run many containers from one image, each isolated from the others — this distinction underpins everything in Week 2.

5. Hands-on Exercise

Hands-on

Install Docker, run three containers, and observe real isolation

Get comfortable with the basic container lifecycle, then prove to yourself — with real commands, not just reading — that namespaces and cgroups are actually doing something.

Part 1 — Lifecycle basics:

  1. Install Docker and confirm docker info runs without errors.
  2. Run docker run -d --name web1 -p 8081:80 nginx and docker run -d --name web2 -p 8082:80 nginx — two containers from the same image, different host ports.
  3. Use docker ps, docker inspect web1 and docker logs web1 to explore what's running and what Docker knows about it.
  4. Stop and remove both containers, then confirm with docker ps -a that they're gone but docker images still shows the nginx image.
Hint

docker inspect <container> dumps a huge JSON blob — pipe it through docker inspect web1 | grep -i ipaddress to find just the container's IP address, and notice it's different from your host's.

Part 2 — Prove the isolation is real:

Run two containers and verify, hands-on, that PID and network namespaces are actually isolating them from each other.

  1. Start a container: docker run -d --name isolated ubuntu sleep 3600.
  2. Exec into it and run ps aux — count how many processes you see. It should be a tiny handful, not your host's full process list.
  3. From inside the container, run hostname and compare it to your host machine's hostname — note they're different, and that changing one via hostname newname inside the container doesn't touch your host's hostname.
  4. Start a second container (docker run -it ubuntu bash) and, from inside it, try to ping the first container by its container name — it should fail on the default bridge network without extra configuration. Note down why you think that is (you'll confirm the real answer in Week 4).
Hint

If ping isn't installed in the ubuntu image, that's expected — minimal images ship almost nothing by default. Install it with apt update && apt install -y iputils-ping inside the running container, purely for this exercise (you'd never do this in a real image — more on that in Week 6).

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

What's the single biggest structural difference between a VM and a container?

A VM virtualizes hardware and boots a full guest kernel per VM. A container shares the host's kernel and is isolated using namespaces and cgroups — it's a regular process, not a virtualized machine, which is why it starts fast and has a much smaller footprint.

Q2

Which Linux primitive limits how much CPU or memory a container can use, and which controls what it can see?

cgroups limit resource usage (CPU, memory, PIDs, I/O). Namespaces control visibility (its own process tree, network interfaces, filesystem view, hostname). One caps what it can use, the other controls what it can see.

Q3

In the Docker Engine pipeline, what does runc actually do?

runc is the lowest-level component — it's the OCI-compliant tool that actually creates the namespaces and cgroups on the host and execs your process into them. dockerd and containerd sit above it managing the higher-level lifecycle (builds, networks, volumes, monitoring).

Q4

What's the difference between a Docker image and a Docker container?

An image is a read-only, immutable template — the filesystem and metadata needed to start a container. A container is a running (or stopped) instance created from an image, with its own writable layer on top. You can start many independent containers from a single image.