Skip to main content

🐳 Lesson 2: Docker Architecture

Open the hood. See how the docker command you typed in Lesson 1 actually became a running container — the client/daemon split, the core objects, image layers, and the lifecycle that ties it all together.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Diagram Docker's client/server model and explain how the docker CLI talks to the daemon over a REST API
  • Identify Docker's core objects — images, containers, registries, volumes, and networks — and how they relate
  • Explain image layers and the union filesystem, including the container's thin writable layer
  • Trace a container through its lifecycle (pull → create → start → stop → remove) and inspect it with real commands

Estimated Time: 40–50 minutes

Prerequisite: Lesson 1: Introduction to Docker

In This Lesson

The Client/Server Model

When you ran docker run hello-world in Lesson 1, it felt like one program did all the work. It didn't. Docker is split into two pieces that talk to each other:

  • The Docker client (docker) — the command-line tool you type into. It's a thin messenger: it turns your command into an API request and shows you the response.
  • The Docker daemon (dockerd) — the long-running background service that does the real work: building images, pulling from registries, and creating and running containers.

The client talks to the daemon over a REST API. On Linux and macOS that conversation travels over a Unix socket at /var/run/docker.sock; it can also run over a network socket. Because it's just an API, the client and daemon don't have to be on the same machine — your local docker can drive a daemon on a remote host.

The Docker client sends API calls to the daemon on the Docker host, which manages images and containers and talks to a registry Docker Client docker build docker pull docker run the CLI you type REST API /var/run/docker.sock Docker Host Docker Daemon (dockerd) builds, runs, and manages everything Images nginx:latest python:3.12 Containers web (running) db (running) Volumes Networks daemon manages local objects pull / push Registry Docker Hub image storage
Figure 1: The client sends API calls; the daemon on the Docker host does the work — managing images, containers, volumes, and networks, and pulling or pushing images to a registry.

Here is what actually happened inside that one docker run hello-world command from Lesson 1:

sequenceDiagram participant You as "You (terminal)" participant Client as "docker (client)" participant Daemon as "dockerd (daemon)" participant Reg as "Registry (Docker Hub)" You->>Client: docker run hello-world Client->>Daemon: POST /containers/create (REST API) Daemon->>Daemon: Image not found locally Daemon->>Reg: Pull hello-world:latest Reg-->>Daemon: Image layers Daemon->>Daemon: Create + start container Daemon-->>Client: Stream container output Client-->>You: "Hello from Docker!"
💡 Why the split matters: because the client is just an API caller, the same daemon can be driven by the CLI, by Docker Desktop's GUI, by docker compose, or by an automation script. They all speak the same REST API to dockerd.

Core Docker Objects

Everything the daemon manages falls into a handful of object types. Learn these five nouns and the rest of Docker becomes vocabulary you already know.

ObjectWhat it isKitchen analogy
ImageA read-only, layered template that packages an app and its dependenciesThe recipe (immutable)
ContainerA running (or stopped) instance of an image, with a thin writable layer on topThe dish you cooked from the recipe
RegistryA store for images you can pull from or push to (Docker Hub is the default)The cookbook library
VolumeDocker-managed storage that persists data independently of any containerThe pantry that outlives one meal
NetworkA virtual network that lets containers find and talk to each otherThe intercom between stations
An image is the template; a container is a running instance; registries store images; volumes persist data; networks connect containers 📄 Image read-only layered template nginx:1.27 docker run 📦 Container 1 📦 Container 2 one image → many containers 💾 Volume — persistent data 🔌 Network — container-to-container 🗄️ Registry (Docker Hub) pull images from / push images to
Figure 2: The five core objects. Note that a single image can spawn many independent containers.

📖 Definition

Registry vs repository vs tag: a registry (like Docker Hub) hosts many repositories (like library/nginx), and each repository holds multiple tags (like 1.27 or latest). The full name docker.io/library/nginx:1.27 spells out all three.

You'll meet volumes and networks in depth in later modules. For now, remember that they exist outside any single container's lifespan: delete a container and its writable layer vanishes, but a named volume and its data survive.

Under the Daemon: containerd & runc

The daemon doesn't spawn Linux processes all by itself. Modern Docker delegates the low-level work to a layered runtime stack, each piece with one job:

  • dockerd — the high-level daemon. Handles the API, image builds, networking, and volumes.
  • containerd — a container runtime that manages the full container lifecycle: pulling images, managing storage, and supervising running containers. (It's a graduated CNCF project used well beyond Docker.)
  • runc — the low-level tool that actually creates the container by asking the Linux kernel for namespaces and cgroups. It implements the OCI (Open Container Initiative) runtime spec.
graph TD A["docker (CLI client)"] -->|REST API| B["dockerd (Docker daemon)"] B -->|gRPC| C["containerd (runtime)"] C -->|spawns| D["containerd-shim"] D -->|OCI runtime| E["runc"] E -->|namespaces + cgroups| F["Linux kernel"] F --> G["Your running container"] style A fill:#bbf7d0,color:#1e293b style B fill:#bfdbfe,color:#1e293b style E fill:#c7d2fe,color:#1e293b style G fill:#fde68a,color:#1e293b
⚠️ You rarely touch these directly. The takeaway is the separation of concerns: because runc and containerd follow open standards (OCI), a container built with Docker also runs on other OCI-compliant runtimes like those in Kubernetes. Docker isn't a walled garden.

Image Layers & the Union Filesystem

A Docker image isn't one monolithic blob — it's a stack of read-only layers. Each instruction in a Dockerfile (which you'll write in Module 2) adds a layer capturing just the filesystem changes it made. Docker stacks these layers into a single view using a union filesystem (typically OverlayFS).

Read-only image layers stacked from a base OS up to app code, with a thin writable layer added when a container runs Image = read-only layers Thin writable layer added only when a container runs Layer 4 — COPY app code Layer 3 — RUN pip install deps Layer 2 — install Python runtime Layer 1 — base OS (Alpine / Debian) Read-only Read-write ♻️ Layers are shared Two images built on the same base OS store that base once on disk. Unchanged layers are cached, so rebuilds and pulls skip what they already have.
Figure 3: An image is read-only layers stacked bottom-up. Starting a container adds a thin writable layer on top — the only part unique to that container.

Two properties fall out of this design, and they explain a lot of Docker's speed:

  • Layers are cached and shared. If ten images all start FROM debian:12, that base layer is stored once. Pulling a new image only downloads layers you don't already have.
  • Containers are cheap. Starting a container doesn't copy the image. Docker just adds a small writable layer on top of the shared read-only layers. Ten containers from one image share all the read-only layers and differ only in their own thin writable layer.

📖 Copy-on-write

When a running container modifies a file that lives in a read-only layer, the union filesystem copies that file up into the writable layer first, then edits the copy. The original layer is never touched — which is exactly why the same image can back many containers safely. This is called copy-on-write.

⚠️ The writable layer is disposable. Anything written inside a container (not into a volume) lives only in that writable layer and is gone the moment you docker rm the container. That's why persistent data belongs in a volume, not the container's own filesystem.

The Container Lifecycle

A container moves through a predictable set of states. The convenience command docker run quietly stitches the first few together — understanding the individual steps demystifies what it does.

Container lifecycle: pull an image, create a container, start it to run, stop it, then remove it Pull image on disk Create container (idle) Start running ▶ Stop stopped (state kept) Remove gone 🗑️ docker start (resume) docker run = pull (if needed) + create + start
Figure 4: The container lifecycle. docker run bundles pull + create + start; a stopped container can be restarted or removed.

Mapped to commands:

StepCommandWhat happens
Pulldocker pull nginxDownloads the image's layers to local disk
Createdocker create nginxMakes a container (writable layer + config) but does not start it
Startdocker start <id>Runs the container's process
Stopdocker stop <id>Sends SIGTERM, then SIGKILL — process ends, writable layer preserved
Removedocker rm <id>Deletes the container and its writable layer for good

So the single command from Lesson 1, docker run hello-world, is really pull (because the image was missing) → create → start, all in one line. Now you can see why the second run was instant: the pull step was skipped because the image was already cached.

Hands-On: Inspect the Engine

Let's confirm all of this on your own machine. These commands are read-only — they only report state, so they're safe to run anytime.

1. See the client/daemon split

docker version prints two blocks — one for the Client and one for the Server (the daemon). Seeing them side by side is the client/server model made concrete:

docker version

Output:

Client:
 Version:           27.3.1
 API version:       1.47
 Go version:        go1.22.7
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          27.3.1
  API version:      1.47 (minimum version 1.24)
 containerd:
  Version:          1.7.22
 runc:
  Version:          1.1.14

Notice the server block lists containerd and runc — the exact runtime stack you saw in the previous section.

2. Survey the whole host

docker info summarizes the daemon's world: how many images and containers exist, which storage driver backs the union filesystem, and system resources.

docker info

Output (trimmed):

Client:
 Version:    27.3.1
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
  compose: Docker Compose (Docker Inc.)

Server:
 Containers: 3
  Running: 1
  Paused: 0
  Stopped: 2
 Images: 12
 Server Version: 27.3.1
 Storage Driver: overlayfs
 Default Runtime: runc
 Kernel Version: 6.8.0-45-generic
 Total Memory: 15.4GiB

The Storage Driver: overlayfs line is the union filesystem from Section 4, in the flesh. The compose and buildx plugins are why modern Docker uses docker compose (a plugin, one word with a space) rather than the old standalone docker-compose binary.

3. Peel apart an image's layers

docker image history lists the layers of an image, newest on top. Each row is one Dockerfile instruction that produced a layer:

docker pull nginx
docker image history nginx

Output (trimmed):

IMAGE          CREATED       CREATED BY                                      SIZE
a9d06877f4a1   2 weeks ago   CMD ["nginx" "-g" "daemon off;"]                0B
<missing>      2 weeks ago   EXPOSE map[80/tcp:{}]                           0B
<missing>      2 weeks ago   COPY docker-entrypoint.sh / # buildkit          7.6kB
<missing>      2 weeks ago   RUN /bin/sh -c set -x  && groupadd ...          109MB
<missing>      3 weeks ago   /bin/sh -c #(nop)  ENV NGINX_VERSION=1.27.2      0B
<missing>      3 weeks ago   /bin/sh -c #(nop) ADD file:... in /             77.9MB

Read it bottom-up and you can see the image being built: a base filesystem is added, packages are installed, config is copied, a port is declared, and finally a start command is set. The 0B rows are metadata-only layers (like CMD and EXPOSE) — they change configuration without adding files.

🏋️ Exercise: Watch the lifecycle by hand

Objective: Drive a container through each lifecycle state yourself, instead of letting docker run hide the steps.

Instructions:

  1. Create without starting: docker create --name web nginx
  2. Confirm it exists but isn't running: docker ps -a (look for status Created).
  3. Start it: docker start web, then docker ps to see it running.
  4. Stop it: docker stop web.
  5. Remove it: docker rm web, then docker ps -a to confirm it's gone.
💡 Hint

docker ps shows only running containers; add -a to include stopped and created ones. If docker stop feels slow, that's the 10-second grace period between SIGTERM and SIGKILL.

✅ What you should observe

After create, the container shows status Created and never ran. start flips it to Up; stop to Exited; and after rm it disappears from docker ps -a entirely — its writable layer deleted. You just performed by hand exactly what docker run does in one shot.

Mental Model & Best Practices

You now have the whole map. A few habits will keep it useful as the course gets more hands-on:

  • Think client → daemon, always. When a command behaves oddly, ask which side is at fault. "Cannot connect to the Docker daemon" almost always means dockerd isn't running — not that your CLI is broken.
  • Treat images as immutable, containers as disposable. Don't hand-edit files inside a running container to "fix" it — those changes die with the container. Change the image (via a Dockerfile) and re-run.
  • Order Dockerfile steps from least to most frequently changed. Because layers are cached, putting stable steps (installing dependencies) before volatile ones (copying your code) means rebuilds reuse the cache and finish faster. You'll practice this in Module 2.
  • Store real data in volumes, never the writable layer. The writable layer is scratch space; volumes are the durable pantry.
  • Scan images with docker scout. The old docker scan command has been removed; docker scout cves nginx is the current way to check an image for known vulnerabilities.
  • Trust the standards. Because Docker builds on OCI, containerd, and runc, the images you build here run on Kubernetes and other platforms unchanged. You're learning portable skills, not a single vendor's quirks.

✅ The one-sentence summary

You type a command into the client, which sends it over an API to the daemon, which uses containerd and runc to turn layered read-only images into containers (each with a thin writable layer), pulling from and pushing to registries along the way.

Quick Quiz

Question 1: When you type docker run, which component actually creates and runs the container?

Question 2: What does starting a container add on top of the image's read-only layers?

Question 3: Where should persistent data that must survive a container's removal be stored?

Summary

🎉 Key Takeaways

  • Docker is client/server. The docker CLI sends API requests over a socket to the dockerd daemon, which does the real work.
  • Five core objects — images, containers, registries, volumes, and networks — cover everything the daemon manages.
  • Under the daemon, containerd and runc (following OCI standards) turn images into real Linux processes using namespaces and cgroups.
  • Images are stacked read-only layers that are cached and shared; a running container adds only a thin writable layer via copy-on-write.
  • The lifecycle is pull → create → start → stop → remove, and docker run bundles the first three into one command.

📚 Additional Resources

🚀 What's Next?

You know how the engine is built. Time to drive it. In the next lesson you'll run, name, and manage your first real containers — putting the lifecycle you just learned into your fingers, one command at a time.

🎉 Lesson 2 complete!

The architecture is no longer a black box. Everything else in this course is now just details filling in a map you already have.