๐ณ Docker Architecture and Components
When you type docker run, a small chain of programs springs into action behind the scenes. This lesson opens the hood on Docker's client-server design so that when something misbehaves later, you know exactly which part to look at.
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Explain Docker's client-server architecture and the role of the daemon
- Describe how containerd and runc divide the runtime work
- Explain image layers, the union filesystem, and copy-on-write
- Choose between volumes, bind mounts, and tmpfs for container data
- Identify Docker's network drivers and how containers find each other
- Read a Docker Compose file describing a multi-container app
Estimated Time: 35โ45 minutes โข Difficulty: Intermediate
Hands-on: Trace a docker run command through every architectural layer.
In This Lesson
The Docker Platform
Docker is a platform for building, packaging, and running applications in containers. It's practically synonymous with containerization because it made containers usable for everyday development, not just for kernel specialists. In the last lesson you learned what containers are; now you'll learn how Docker actually runs them.
The platform is more than one program. At a high level it includes the Docker Engine (the core runtime), the CLI client you type commands into, Docker Compose for multi-container apps, Docker Desktop (the GUI bundle for Windows/macOS), and Docker Hub (the default registry).
Client-Server Architecture
Docker uses a client-server architecture. The Docker client (the docker command) talks to the Docker daemon (dockerd) over a REST API. The daemon does the real work โ building images, running containers, managing networks and volumes. Because the API is network-capable, the client can even control a daemon on a different machine.
๐บ Remote and TV: The client is the remote control โ you press buttons (issue commands). The daemon is the TV's internal circuitry โ it does the actual work of changing channels and adjusting volume. Pressing the remote doesn't do anything by itself; it just sends a signal.
The core components
- Docker Client โ the CLI/API you interact with
- Docker Daemon (dockerd) โ the background service that manages all Docker objects
- containerd โ a runtime that manages the container lifecycle
- runc โ the low-level runtime that creates the container by talking to the kernel
- Registry โ stores and distributes images
Common client commands
# Run a container from the nginx image
docker run nginx
# List running containers
docker ps
# Build an image from a Dockerfile in the current directory
docker build -t myapp .
# Pull an image from a registry
docker pull ubuntu:22.04
# Push an image to a registry
docker push myusername/myapp:1.0
โ ๏ธ The daemon runs as root
The Docker daemon runs with root privileges, so anyone who can send it commands effectively has root on the host. Treat access to the Docker socket as equivalent to root access: use TLS for any remote connections, apply least privilege, and keep Docker updated.
containerd and runc
Docker didn't stay one monolithic program. Its core runtime was split out into reusable, standardized pieces โ containerd and runc โ which are now governed independently and used by other systems (Kubernetes talks to containerd directly). Understanding the split explains what each layer is responsible for.
containerd
A daemon that manages the complete container lifecycle on a host: pulling and storing images, and starting, supervising, and stopping containers. If dockerd is the head chef coordinating the kitchen, containerd is the station chef who actually runs the cooking processes.
runc
A lightweight runtime implementing the Open Container Initiative (OCI) spec. It does the low-level work of creating a container: setting up Linux namespaces and cgroups, configuring capabilities and the filesystem, then executing the container process. runc is the cook preparing an individual dish from a precise recipe.
containerd-shim
A tiny process that sits between containerd and the running container. It lets containers keep running even if containerd restarts, reports the container's exit status back, and keeps the I/O streams open. Think of it as the kitchen expediter making sure finished plates are delivered even when the chef is slammed.
๐ Namespaces & cgroups
Namespaces give a container its own isolated view of the system โ its own process list, network, and mounts โ so it can't see its neighbors.
cgroups (control groups) limit how much CPU, memory, and I/O a container may consume, so one container can't starve the others.
These two Linux kernel features are the real foundation containers are built on.
Images, Layers & Copy-on-Write
Docker images are read-only templates built from stacked filesystem layers. Each instruction in a Dockerfile produces one layer capturing just the changes from the step before it.
# Layer 1: base image
FROM ubuntu:22.04
# Layer 2: install Node.js
RUN apt-get update && apt-get install -y nodejs npm
# Layer 3: set the working directory
WORKDIR /app
# Layer 4: copy application code
COPY . .
# Layer 5: install dependencies
RUN npm ci
# Layer 6: declare the port
EXPOSE 3000
# Layer 7: startup command
CMD ["npm", "start"]
Union filesystem
Docker uses a union filesystem to stack these read-only layers into one coherent view. It's like transparent overlays in an image editor: each sheet sits on the ones below it, and the topmost version of a file wins.
Copy-on-write
When a container runs, Docker adds a thin writable layer on top of the read-only image layers. Changes use a copy-on-write mechanism:
- Reading an unmodified file reads straight from the lower image layers.
- Modifying a file first copies it up into the writable layer, then edits the copy.
- All later reads see the modified copy from the writable layer.
๐ The photocopy analogy: Instead of writing on the original document, you make a photocopy and mark that up. Your notes are all yours; the pristine original stays available for everyone else โ which is exactly why many containers can safely share one image.
โ ๏ธ The writable layer is disposable
Everything written to a container's writable layer disappears when that container is removed. This is by design โ and the reason the next section on persistent storage exists.
Storage: Volumes, Binds, tmpfs
Because the writable layer is ephemeral, Docker offers three ways to handle data that needs to outlive โ or bypass โ the container.
Volumes (preferred)
Created and managed by Docker, stored in a Docker-controlled area of the host. Volumes persist independently of any container, can be shared between containers, and are easy to back up.
# Create a named volume
docker volume create my-data
# Mount it into a container
docker run -v my-data:/app/data nginx
# List and inspect volumes
docker volume ls
docker volume inspect my-data
Bind mounts
Map a specific host directory straight into the container. Great for development โ edit code on the host and see it live in the container โ but tied to the host's exact directory layout.
# Map a host path into the container
docker run -v /host/path:/container/path nginx
tmpfs mounts
Store data in the host's RAM only, never on disk. Ideal for fast scratch space or sensitive data that must not persist.
# Mount an in-memory filesystem
docker run --tmpfs /app/temp nginx
๐ Three notebooks: A volume is a dedicated journal that lives on your bookshelf. A bind mount is a sticky note stuck onto a surface you already have. A tmpfs mount is an erasable whiteboard that wipes clean the moment the power goes off.
Networking
Docker gives containers a networking system so they can talk to each other and to the outside world. It ships several network drivers for different situations.
| Driver | What it does | Use it for |
|---|---|---|
| bridge | Default private network on one host; members can talk to each other | Most single-host apps |
| host | Removes isolation; container uses the host's network directly | Max network performance, no port mapping |
| none | Disables networking entirely | Fully isolated batch jobs |
| overlay | Connects containers across multiple hosts | Swarm / multi-host clusters |
| macvlan | Gives a container its own MAC, appearing as a physical device | Legacy apps expecting a real NIC |
Container-to-container communication
On a user-defined bridge network, containers reach each other by name โ Docker runs an embedded DNS server that resolves container names to their current IP addresses. No hard-coded IPs required.
# Create a network and run a web server on it
docker network create my-network
docker run -d --name web --network my-network nginx
# Another container reaches it simply as "web"
docker run --network my-network alpine wget -qO- http://web
โ๏ธ The telephone exchange: Different drivers are like different kinds of phone lines, but they all connect callers (containers) using rules and a directory (DNS) so nobody needs to memorize raw numbers (IPs).
Registries
A registry stores and distributes images. Docker Hub is the default public registry; teams also run private registries or use cloud ones like Amazon ECR, Google Artifact Registry, and Azure Container Registry.
# Pull an image from Docker Hub
docker pull nginx:latest
# Tag a local image for a registry namespace
docker tag my-app:1.0 username/my-app:1.0
# Authenticate, then push
docker login
docker push username/my-app:1.0
# Pull from a private registry by hostname
docker pull registry.example.com/my-app:1.0
A registry works like a package-distribution center: developers deliver packaged apps (images), the center shelves them in organized repositories, and delivers them to users on request.
Docker Compose
Real apps rarely run in a single container โ you might have a web server, an app, and a database that all need to start together and talk to each other. Docker Compose describes that whole stack in one YAML file and brings it up with one command.
# compose.yaml
services:
# Reverse-proxy / static web server
web:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./website:/usr/share/nginx/html
depends_on:
- app
# Application server built from a local Dockerfile
app:
build: ./app
environment:
- NODE_ENV=production
- DB_HOST=db
depends_on:
- db
# Database with a named volume for persistence
db:
image: postgres:16
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=change_me
- POSTGRES_USER=myuser
- POSTGRES_DB=myapp
volumes:
postgres_data:
# Start the whole stack in the background
docker compose up -d
# View logs, stop everything, or scale a service
docker compose logs -f
docker compose down
docker compose up -d --scale app=3
๐ก Note: docker compose, not docker-compose
Modern Docker ships Compose V2 as a built-in CLI plugin, so the command is docker compose (a space, no hyphen) and the file is compose.yaml. The old standalone docker-compose binary and the top-level version: key are legacy โ you'll still see them in older tutorials, but you don't need them.
Compose is a blueprint plus a construction manager: the YAML is the blueprint specifying how everything connects, and the up command is the manager that builds and wires it all together to plan.
Hands-on Exercise
๐๏ธ Trace a docker run Through the Stack
Objective: Cement the architecture by following one command through every component.
Scenario:
You type docker run -d --name web -p 8080:80 nginx on a fresh machine that has never pulled the nginx image. Write down, in order, what each component does.
- What does the client do first?
- The image isn't local yet โ where does the daemon get it, and how do layers come into play?
- Which components actually create and start the running container?
- What does
-p 8080:80configure, and which subsystem handles it?
๐ก Hint
Walk down the chain from Figure 1 and the containerd/runc diagram: client โ daemon โ registry (for the pull) โ containerd โ shim โ runc โ kernel. Remember that networking (port mapping) and storage are daemon subsystems.
โ Solution
- The client serializes the command and sends it to the daemon over the REST API.
- The daemon sees the image is missing and pulls
nginxfrom Docker Hub, downloading only the layers not already cached and assembling them via the union filesystem into a read-only image. - The daemon hands off to containerd, which starts a containerd-shim; the shim invokes runc, which sets up namespaces and cgroups in the kernel and executes nginx. A writable copy-on-write layer is added on top.
-p 8080:80maps host port 8080 to container port 80; the daemon's networking subsystem (default bridge driver) sets up the port forwarding rule.
๐ฏ Quick Quiz
Question 1: In Docker's client-server architecture, which component actually creates and runs containers?
Question 2: You need data that survives after its container is deleted and is easy to back up. Which storage option fits best?
Question 3: Two containers on the same user-defined bridge network need to talk. How does one reach the other?
Summary & Quiz
๐ Key Takeaways
- Docker uses a client-server design: the client sends commands, the daemon does the work.
- The daemon delegates to containerd (lifecycle) and runc (low-level, OCI, kernel namespaces & cgroups).
- Images are read-only layers; each container adds a disposable copy-on-write writable layer.
- Persist data with volumes (preferred), bind mounts (dev), or tmpfs (in-memory).
- Network drivers (bridge, host, overlay, โฆ) connect containers; embedded DNS resolves them by name.
- Docker Compose defines multi-container apps in one YAML file โ use
docker compose(V2).
๐ Further Reading
๐ What's Next?
Theory in place, it's time to get Docker actually running on your machine. Next we cover installation and configuration across Windows, macOS, and Linux โ plus resources, contexts, and security settings.
๐ Nice work!
You can now name every component a docker run touches. Let's install it for real.