Back to Containers & Runtime Environments Mastery Series

Part 5: Docker Architecture & Core Concepts

May 14, 2026 Wasil Zafar 28 min read

Docker transformed raw Linux kernel primitives — namespaces, cgroups, and union filesystems — into an elegant developer experience. This article maps the complete Docker architecture: from the CLI on your laptop through the daemon, containerd, and runc, all the way down to the kernel calls that create your containers.

Table of Contents

  1. The Docker Ecosystem
  2. Docker Engine Architecture
  3. Docker CLI
  4. Docker Daemon (dockerd)
  5. containerd
  6. runc
  7. Core Concept: Images
  8. Core Concept: Containers
  9. Core Concept: Registries
  10. Core Concept: Volumes
  11. Core Concept: Networks
  12. Putting It All Together
  13. Exercises
  14. Conclusion & Next Steps

The Docker Ecosystem Overview

Before Docker, using Linux containers required manual orchestration of namespaces, cgroups, and chroot — an approach accessible only to kernel engineers. Docker's genius was packaging these primitives into a simple CLI workflow: docker build, docker push, docker run. But the Docker you use today is a composite of many components, some controlled by Docker Inc. and others donated to open-source foundations.

Open Source vs Commercial

Component Owner License Purpose
Docker CLI Docker Inc. (open source) Apache 2.0 Client-side command interface
Docker Engine (dockerd) Docker Inc. (open source) Apache 2.0 API server, image/volume/network management
containerd CNCF (graduated project) Apache 2.0 Container runtime — lifecycle, image, snapshot management
runc Open Containers Initiative (OCI) Apache 2.0 Low-level OCI runtime — creates containers using kernel APIs
Docker Compose Docker Inc. (open source) Apache 2.0 Multi-container application definition and orchestration
Docker Desktop Docker Inc. Proprietary (free for personal/small biz) GUI app for macOS/Windows with embedded Linux VM
Docker Hub Docker Inc. Proprietary (SaaS) Public/private container image registry
BuildKit Docker Inc. (open source) Apache 2.0 Next-generation image builder with parallelism and caching
Docker Ecosystem Map
flowchart TB
    subgraph user["User Layer"]
        CLI["Docker CLI"]
        Compose["Docker Compose"]
        Desktop["Docker Desktop
(macOS/Windows)"] end subgraph engine["Docker Engine"] Daemon["dockerd
(API Server)"] BuildKit["BuildKit
(Image Builder)"] end subgraph runtime["Container Runtime"] CTD["containerd
(CNCF Graduated)"] Shim["containerd-shim"] RunC["runc
(OCI Runtime)"] end subgraph kernel["Linux Kernel"] NS["Namespaces"] CG["cgroups"] OFS["OverlayFS"] SEC["seccomp / AppArmor"] end subgraph registry["Registry"] Hub["Docker Hub"] Private["Private Registry
(Harbor, ECR, ACR)"] end CLI --> Daemon Compose --> Daemon Desktop --> Daemon Daemon --> CTD Daemon --> BuildKit CTD --> Shim --> RunC RunC --> NS & CG & OFS & SEC Daemon <--> Hub Daemon <--> Private
Historical Evolution: Docker's original architecture was a monolith — a single binary (docker) that did everything. Starting in 2016, Docker Inc. began disaggregating components: first donating containerd to the CNCF, then runc to the OCI. This separation of concerns means Kubernetes can use containerd directly (without Docker), and alternative tools like Podman can use the same runc binary. Docker's value today is primarily in developer experience (CLI, Compose, Desktop) rather than the runtime itself.

Docker Engine Architecture

The Docker Engine follows a client-server architecture with three primary tiers, each communicating through well-defined APIs:

Communication Protocols

From To Protocol Default Socket
Docker CLI dockerd HTTP REST API (over Unix socket or TCP) /var/run/docker.sock
dockerd containerd gRPC (Protocol Buffers) /run/containerd/containerd.sock
containerd runc Exec binary (fork/exec with OCI bundle path) N/A (binary invocation)
containerd shim ttrpc (lightweight gRPC variant) Abstract Unix socket per container
Docker Engine Internal Architecture
sequenceDiagram
    participant User
    participant CLI as Docker CLI
    participant API as dockerd (REST API)
    participant CTD as containerd (gRPC)
    participant Shim as containerd-shim
    participant RunC as runc
    participant Kernel as Linux Kernel

    User->>CLI: docker run nginx
    CLI->>API: POST /containers/create
    API->>API: Resolve image, prepare config
    API->>CTD: Create container (gRPC)
    CTD->>CTD: Prepare snapshot (OverlayFS layers)
    CTD->>Shim: Start shim process
    Shim->>RunC: Fork+exec runc create
    RunC->>Kernel: clone() with namespaces
    RunC->>Kernel: Write cgroup limits
    RunC->>Kernel: Mount OverlayFS
    RunC->>Kernel: pivot_root()
    RunC-->>Shim: Container PID
    Note over RunC: runc exits after creation
    Shim-->>CTD: Container started
    CTD-->>API: Container ID
    API-->>CLI: Container ID
    CLI-->>User: abc123def456
                            

Docker CLI

The Docker CLI (docker) is a Go binary that translates user commands into HTTP REST API calls to the Docker daemon. It's a pure client — it never creates containers itself. This separation means you can control a remote Docker daemon from your local machine, or even use alternative clients that speak the same API.

# The CLI communicates with the daemon via a Unix socket by default
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker 0 May 14 08:00 /var/run/docker.sock

# You can manually call the API the same way the CLI does
curl --unix-socket /var/run/docker.sock http://localhost/version | jq .
# {
#   "Platform": { "Name": "Docker Engine - Community" },
#   "Version": "27.0.3",
#   "ApiVersion": "1.46",
#   "Os": "linux",
#   "Arch": "amd64",
#   "KernelVersion": "6.5.0-35-generic",
#   "GoVersion": "go1.22.3"
# }

# List containers via raw API call (same as `docker ps`)
curl --unix-socket /var/run/docker.sock http://localhost/containers/json | jq .[].Names
# ["/my-nginx"]
# ["/redis-cache"]
# Connect to a REMOTE Docker daemon (e.g., on a build server)
export DOCKER_HOST=tcp://build-server.internal:2376
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH=~/.docker/certs

# Now all docker commands target the remote machine
docker ps   # Shows containers on build-server, not localhost

# SSH-based remote access (more secure, no daemon TCP exposure)
export DOCKER_HOST=ssh://user@build-server.internal
docker ps   # Tunnels through SSH, no TLS config needed

# Docker contexts: save multiple remote configurations
docker context create production --docker "host=ssh://deploy@prod.example.com"
docker context create staging --docker "host=ssh://deploy@staging.example.com"
docker context use production
docker ps   # Now targeting production server
Security Note: The Docker socket (/var/run/docker.sock) grants root-equivalent access to the host. Any process that can communicate with the socket can mount the host filesystem, access all networks, and escalate to root. Never mount the Docker socket into untrusted containers. In production, use TLS mutual authentication for remote daemon access, or SSH tunneling as a more secure alternative.

Docker Daemon (dockerd)

The Docker daemon (dockerd) is a long-running background process that serves the Docker API and orchestrates all Docker operations. It manages:

  • Image management — Pulling, building, tagging, pushing, layer caching
  • Container lifecycle — Delegated to containerd, but dockerd maintains state and metadata
  • Volume management — Creating, mounting, and garbage-collecting volumes
  • Network management — Creating bridges, managing iptables rules, DNS resolution
  • Plugin management — Volume drivers, network drivers, authorization plugins
  • Build operations — Coordinating BuildKit for image builds
# View the daemon process
ps aux | grep dockerd
# root  1234  0.1  0.5 1923456 45678 ?  Ssl  08:00  0:03 /usr/bin/dockerd
#   -H fd:// --containerd=/run/containerd/containerd.sock

# Check daemon status via systemd
systemctl status docker
# ● docker.service - Docker Application Container Engine
#      Loaded: loaded (/lib/systemd/system/docker.service; enabled)
#      Active: active (running) since Wed 2026-05-14 08:00:00 UTC
#    Main PID: 1234 (dockerd)

# View daemon logs
journalctl -u docker --since "1 hour ago" --no-pager | tail -20
// /etc/docker/daemon.json — Daemon configuration
{
    "storage-driver": "overlay2",
    "log-driver": "json-file",
    "log-opts": {
        "max-size": "10m",
        "max-file": "3"
    },
    "default-address-pools": [
        { "base": "172.20.0.0/16", "size": 24 }
    ],
    "dns": ["8.8.8.8", "8.8.4.4"],
    "live-restore": true,
    "userland-proxy": false,
    "experimental": false,
    "metrics-addr": "127.0.0.1:9323",
    "insecure-registries": [],
    "registry-mirrors": ["https://mirror.gcr.io"]
}
Live Restore: The "live-restore": true setting is crucial for production. It allows running containers to continue operating even when the Docker daemon is stopped or restarted (for upgrades). Without it, stopping dockerd would kill all containers — disastrous in production. The containers are kept alive by containerd-shim processes that survive daemon restarts.

containerd

containerd (pronounced "container-dee") is an industry-standard container runtime that manages the complete container lifecycle on a host. Originally built as a Docker component, it was donated to the Cloud Native Computing Foundation (CNCF) in 2017 and graduated in 2019 — a testament to its maturity and wide adoption.

containerd handles:

  • Image pull/push — Fetching layers from registries, content-addressable storage
  • Snapshot management — Preparing OverlayFS mounts for containers
  • Container lifecycle — Create, start, stop, delete containers via shims
  • Task management — Tracking running processes inside containers
  • Content store — Deduplicating and storing image layer blobs
  • Events — Publishing container lifecycle events for monitoring
# containerd runs as its own systemd service
systemctl status containerd
# ● containerd.service - containerd container runtime
#      Active: active (running)

# Use ctr (containerd's native CLI) to interact directly
# List containers managed by containerd
sudo ctr --namespace moby containers list
# CONTAINER         IMAGE                           RUNTIME
# abc123def456      docker.io/library/nginx:latest  io.containerd.runc.v2

# List images in containerd's store
sudo ctr --namespace moby images list | head -5
# REF                            TYPE                   DIGEST       SIZE
# docker.io/library/nginx:latest application/vnd.oci... sha256:3b2... 67.3 MiB

# View containerd's namespaces (Docker uses "moby" namespace)
sudo ctr namespaces list
# NAME    LABELS
# moby
# k8s.io  (if Kubernetes is also using containerd)
Industry Impact

containerd: From Docker Component to Industry Standard

containerd's independence from Docker was a pivotal moment in container history. Key milestones:

  • 2016 — Docker extracts containerd as a separate daemon
  • 2017 — Donated to CNCF; Kubernetes adds containerd as a CRI runtime option
  • 2019 — CNCF graduation (same status as Kubernetes itself)
  • 2022 — Kubernetes removes dockershim; containerd becomes the primary runtime
  • 2024+ — Used by AWS Fargate, Google GKE, Azure AKS, most cloud container services

This means you can run containers without Docker installed at all — just containerd + runc. Kubernetes clusters typically have no Docker daemon; they communicate with containerd directly via the Container Runtime Interface (CRI).

CNCF Kubernetes CRI

runc

runc is the lowest-level component — a lightweight CLI tool that creates and runs containers according to the OCI (Open Container Initiative) Runtime Specification. It's the component that actually calls clone() with namespace flags, writes cgroup configuration, mounts OverlayFS, and executes pivot_root() to enter the container.

# runc is a standalone binary
which runc
# /usr/bin/runc

runc --version
# runc version 1.1.12
# commit: v1.1.12-0-g51d5e946
# spec: 1.0.2-dev
# go: go1.22.3
# libseccomp: 2.5.4

# runc operates on OCI bundles — a directory with:
#   config.json  (OCI runtime spec — namespaces, mounts, cgroups)
#   rootfs/      (the container's root filesystem)

# View what runc does (educational — normally containerd handles this)
sudo runc spec    # Generate a default OCI config.json
cat config.json | jq '.linux.namespaces'
# [
#   { "type": "pid" },
#   { "type": "network" },
#   { "type": "ipc" },
#   { "type": "uts" },
#   { "type": "mount" },
#   { "type": "cgroup" }
# ]
Key Design Principle — runc Exits After Creation: runc does not stay running as a long-lived daemon. It creates the container, sets up all kernel resources, starts the container's init process, then exits. The container process is then "reparented" to the containerd-shim, which monitors it for the rest of its lifecycle. This means runc can be upgraded without affecting running containers — a critical property for production systems.

Core Concept: Images

A Docker image is an immutable, ordered collection of filesystem layers plus metadata (environment variables, default command, exposed ports, labels). Images are identified by two mechanisms:

  • Tags — Human-readable names like nginx:1.25 or myapp:latest (mutable — can be reassigned)
  • Digests — Content-addressable SHA256 hashes like sha256:3b25b682ea... (immutable — content defines identity)
# Pull an image (downloads layers not already cached)
docker pull nginx:1.25
# 1.25: Pulling from library/nginx
# bd159e379b3b: Already exists     ← cached base layer
# 6d5b3ea3b509: Pull complete      ← new layer downloaded
# Digest: sha256:3b25b682ea82b2db3...
# Status: Downloaded newer image for nginx:1.25

# List local images
docker images
# REPOSITORY   TAG      IMAGE ID       CREATED      SIZE
# nginx        1.25     3b25b682ea82   2 weeks ago  187MB
# nginx        latest   3b25b682ea82   2 weeks ago  187MB  ← same image!
# node         18-slim  a1b2c3d4e5f6   3 weeks ago  243MB

# Inspect image metadata
docker image inspect nginx:1.25 --format '{{json .Config}}' | jq .
# {
#   "Hostname": "",
#   "Env": ["PATH=/usr/local/sbin:...", "NGINX_VERSION=1.25.5"],
#   "Cmd": ["nginx", "-g", "daemon off;"],
#   "ExposedPorts": { "80/tcp": {} },
#   "Labels": { "maintainer": "NGINX Docker Maintainers" }
# }

# Pin to a specific digest for reproducible deployments
docker pull nginx@sha256:3b25b682ea82b2db3cc4...
# This will ALWAYS pull the exact same image, regardless of tag changes

Core Concept: Containers

A container is a running (or stopped) instance of an image. It adds a writable layer on top of the image's read-only layers and maintains runtime state (PID, network config, environment variables, mounted volumes). Containers have a defined lifecycle with distinct states:

State Description Transition Commands
Created Container configured but process not started docker create → Created
Running Main process is active docker start or docker run
Paused Process frozen via cgroup freezer (SIGSTOP) docker pause / docker unpause
Stopped Main process exited (exit code preserved) docker stop (SIGTERM→SIGKILL) or process exits
Deleted Container metadata and writable layer removed docker rm
# Complete container lifecycle demonstration
# 1. CREATE — configure but don't start
docker create --name lifecycle-demo -p 8080:80 nginx:latest
# abc123def456...  (container ID returned, state: Created)

# 2. START — begin running the container process
docker start lifecycle-demo
# lifecycle-demo  (state: Running)

# 3. PAUSE — freeze the process (zero CPU, memory preserved)
docker pause lifecycle-demo
docker inspect lifecycle-demo --format '{{.State.Status}}'
# paused

# 4. UNPAUSE — resume execution
docker unpause lifecycle-demo

# 5. STOP — graceful shutdown (SIGTERM, then SIGKILL after 10s)
docker stop lifecycle-demo
docker inspect lifecycle-demo --format '{{.State.ExitCode}}'
# 0  (clean shutdown)

# 6. RESTART — stop + start in one command
docker restart lifecycle-demo

# 7. REMOVE — delete container and its writable layer
docker stop lifecycle-demo
docker rm lifecycle-demo

# One-shot: run + auto-remove on exit
docker run --rm --name temp nginx:latest echo "hello"
# hello  (container automatically deleted after echo exits)

Core Concept: Registries

A container registry is a server that stores and distributes container images. Registries implement the OCI Distribution Specification (originally the Docker Registry HTTP API V2), providing endpoints for pushing, pulling, and discovering images.

Registry Provider Use Case Free Tier
Docker Hub Docker Inc. Public images, official images, personal projects Unlimited public repos, 1 private repo
GitHub Container Registry (ghcr.io) GitHub/Microsoft Open source projects, GitHub Actions CI/CD Unlimited public, generous private
Amazon ECR AWS Production workloads on AWS (ECS, EKS, Lambda) 500 MB storage free tier
Azure Container Registry Microsoft Production workloads on Azure (AKS, Container Apps) Basic tier included in subscription
Google Artifact Registry Google Cloud Production workloads on GCP (GKE, Cloud Run) 500 MB storage free tier
Harbor CNCF (open source) Self-hosted enterprise registry with scanning, RBAC Self-hosted (free software)
Quay.io Red Hat Red Hat ecosystem, OpenShift integration Unlimited public repos
# Tag and push an image to a registry
docker tag myapp:latest ghcr.io/username/myapp:v1.2.3
docker push ghcr.io/username/myapp:v1.2.3
# The push refers to repository [ghcr.io/username/myapp]
# 5f70bf18a086: Pushed    ← only unique layers are uploaded
# 3b25b682ea82: Mounted   ← shared layer already exists in registry
# v1.2.3: digest: sha256:abc123... size: 1234

# Login to a private registry
docker login ghcr.io -u username
# Password: (enter personal access token)
# Login Succeeded

# Pull from a specific registry
docker pull ghcr.io/username/myapp:v1.2.3

# Run a local registry for development/testing
docker run -d -p 5000:5000 --name registry registry:2
docker tag myapp:latest localhost:5000/myapp:latest
docker push localhost:5000/myapp:latest

Core Concept: Volumes

Volumes solve the ephemeral container layer problem — they provide persistent storage that survives container deletion. Docker supports three storage mechanisms:

Type Syntax Managed By Use Case
Named Volume -v mydata:/app/data Docker (stored in /var/lib/docker/volumes/) Database storage, persistent application data
Bind Mount -v /host/path:/container/path User (host filesystem) Development (live code reload), sharing host files
tmpfs Mount --tmpfs /tmp Kernel (RAM-backed) Sensitive data that shouldn't persist, fast scratch space
# Named volumes: Docker manages the storage location
docker volume create app-data
docker run -d --name db -v app-data:/var/lib/postgresql/data postgres:16
# Data persists even after: docker rm db

# Inspect a volume
docker volume inspect app-data
# [{ "Name": "app-data",
#    "Driver": "local",
#    "Mountpoint": "/var/lib/docker/volumes/app-data/_data",
#    "CreatedAt": "2026-05-14T10:00:00Z" }]

# Bind mounts: map host directory into container
docker run -d --name dev-app \
    -v $(pwd)/src:/app/src \
    -v $(pwd)/config.yml:/app/config.yml:ro \
    node:18 npm run dev
# :ro = read-only inside container (can't accidentally modify host file)

# tmpfs: RAM-backed temporary storage (never written to disk)
docker run -d --name secure-app \
    --tmpfs /tmp:size=100m \
    --tmpfs /run/secrets:size=1m,mode=0700 \
    myapp:latest
Production Rule: Always use named volumes for database containers (PostgreSQL, MySQL, MongoDB, Redis). Never rely on the container's writable layer for data you care about. Named volumes persist across container recreations (docker-compose down && docker-compose up) and can be backed up independently with docker run --rm -v app-data:/data -v /backup:/backup alpine tar czf /backup/snapshot.tar.gz /data.

Core Concept: Networks

Docker networking provides containers with isolated network stacks (from network namespaces) while allowing controlled communication between containers and the outside world. Docker ships with several network drivers:

Driver Scope DNS Discovery Use Case
bridge Single host Yes (user-defined bridges) Default for standalone containers; isolated communication
host Single host N/A (uses host network) Maximum network performance; no isolation
overlay Multi-host (Swarm) Yes Cross-host container communication in clusters
macvlan Single host No Container gets its own MAC address on physical network
none N/A No Complete network isolation (security-sensitive workloads)
# Create a user-defined bridge network
docker network create --driver bridge --subnet 172.25.0.0/16 app-network

# Run containers on the same network (they can reach each other by name)
docker run -d --name api --network app-network myapi:latest
docker run -d --name db --network app-network postgres:16

# The 'api' container can reach the database via DNS name 'db'
docker exec api ping db
# PING db (172.25.0.3): 56 data bytes
# 64 bytes from 172.25.0.3: seq=0 ttl=64 time=0.067 ms

# Inspect network details
docker network inspect app-network --format '{{json .Containers}}' | jq .
# {
#   "abc123": { "Name": "api", "IPv4Address": "172.25.0.2/16" },
#   "def456": { "Name": "db",  "IPv4Address": "172.25.0.3/16" }
# }

# List all networks
docker network ls
# NETWORK ID     NAME          DRIVER    SCOPE
# a1b2c3d4e5f6   bridge        bridge    local   ← default (no DNS)
# f6e5d4c3b2a1   host          host      local
# 1a2b3c4d5e6f   none          null      local
# 9f8e7d6c5b4a   app-network   bridge    local   ← user-defined (DNS works)
Networking Tip

Default Bridge vs User-Defined Bridge

The default bridge network (docker0) has a critical limitation: containers can only communicate by IP address, not by name. User-defined bridges provide automatic DNS resolution — containers can reach each other by container name. This is why Docker Compose always creates a dedicated network for its services.

  • Default bridge: docker run nginx → no DNS, manual --link (deprecated), all containers share one bridge
  • User-defined bridge: docker network create mynet → automatic DNS, network isolation, better iptables rules

Always create user-defined networks for multi-container applications. The default bridge exists only for backwards compatibility.

DNS Service Discovery Isolation

Putting It All Together

Now that we understand each component, let's trace the complete journey of docker run -d -p 8080:80 --name web nginx:latest — from keypress to running container:

Complete Flow: docker run nginx
sequenceDiagram
    participant U as User Terminal
    participant CLI as docker CLI
    participant D as dockerd
    participant CTD as containerd
    participant S as containerd-shim
    participant R as runc
    participant K as Linux Kernel
    participant Reg as Docker Hub

    U->>CLI: docker run -d -p 8080:80 nginx
    CLI->>D: POST /images/create?fromImage=nginx&tag=latest
    D->>Reg: GET /v2/library/nginx/manifests/latest
    Reg-->>D: Image manifest (layer digests)
    D->>Reg: GET /v2/library/nginx/blobs/sha256:...
    Reg-->>D: Layer data (parallel downloads)
    D-->>CLI: Image pulled successfully

    CLI->>D: POST /containers/create {Image: nginx, HostConfig: {PortBindings: 8080→80}}
    D->>D: Allocate container ID, validate config
    D->>CTD: CreateContainer (gRPC)
    CTD->>CTD: Prepare OverlayFS snapshot (stack image layers)
    D-->>CLI: {Id: "abc123..."}

    CLI->>D: POST /containers/abc123/start
    D->>CTD: StartContainer (gRPC)
    CTD->>S: Fork containerd-shim-runc-v2
    S->>R: exec runc create --bundle /run/containerd/...
    R->>K: clone(CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | ...)
    R->>K: Write /sys/fs/cgroup/.../memory.max, cpu.max
    R->>K: mount("overlay", "/merged", ...)
    R->>K: pivot_root("/merged", "/merged/.pivot")
    R->>K: execve("/docker-entrypoint.sh")
    Note over R: runc exits (process reparented to shim)
    S-->>CTD: Container PID = 4567
    D->>K: iptables -t nat -A DOCKER -p tcp --dport 8080 -j DNAT --to 172.17.0.2:80
    CTD-->>D: Started
    D-->>CLI: 204 No Content
    CLI-->>U: abc123def456 (container running!)
                            

The entire process from command to running container takes roughly 1-3 seconds (depending on whether the image is cached). Here's what each phase accomplished:

  1. Image resolution — CLI asks daemon, daemon checks local cache, pulls missing layers from registry
  2. Container creation — Daemon allocates ID and metadata, containerd prepares the OverlayFS mount
  3. Container start — Shim forks runc, runc creates namespaces and cgroups, starts the process, then exits
  4. Networking — Daemon configures iptables port forwarding from host:8080 to container:80
  5. Monitoring — Shim keeps the container running and reports status back to containerd/daemon
Why the Shim Matters: The containerd-shim keeps running even if containerd or dockerd crashes/restarts. It holds the container's stdio file descriptors, waits for the container PID to exit, and reports the exit code. This decoupling means: (1) daemon upgrades don't kill running containers, (2) each container has its own shim (one crash doesn't cascade), (3) shims use minimal resources (~8 MB RSS each).

Exercises

  1. API Exploration — Use curl with the Docker socket to list containers, inspect an image, and create+start a container entirely through raw API calls (no docker CLI). Document each endpoint you call and the response structure.
  2. containerd Direct Access — Use ctr (containerd's CLI) in the moby namespace to list containers and images. Pull an image directly with ctr images pull (bypassing Docker) and compare with docker images.
  3. Network Lab — Create three user-defined networks (frontend, backend, database). Run an nginx container on frontend, an API container on both frontend and backend, and a PostgreSQL container on database and backend. Verify that nginx cannot directly reach PostgreSQL but the API can reach both.
  4. Volume Persistence Test — Create a PostgreSQL container with a named volume. Insert data. Remove and recreate the container with the same volume. Verify data survives. Then repeat without a volume and confirm data loss.
  5. Architecture Trace — Run docker run -d nginx and then use ps aux, ls /proc/<PID>/ns/, cat /sys/fs/cgroup/.../, and mount | grep overlay to find all the kernel resources (namespaces, cgroups, OverlayFS mount) that were created. Map each resource back to the architectural component that created it.

Conclusion & Next Steps

Docker's architecture is an elegant layering of responsibilities: the CLI provides developer experience, the daemon manages state and coordination, containerd handles runtime lifecycle, and runc interfaces with the kernel. Understanding this separation helps you:

  • Debug effectively — Know which component to investigate when things go wrong
  • Understand Kubernetes — K8s uses containerd directly, bypassing Docker entirely
  • Make architectural decisions — Choose between Docker, Podman, containerd, or other tools
  • Secure your infrastructure — Understand the trust boundaries between components

Key takeaways:

  • Docker CLI is a stateless client that communicates via REST API over a Unix socket
  • dockerd is the API server managing images, volumes, networks, and build operations
  • containerd is a CNCF-graduated runtime handling container lifecycle and image management
  • runc creates containers using kernel namespaces and cgroups, then exits
  • Images are immutable layer stacks; containers add a writable layer on top
  • Volumes provide persistence beyond container lifecycle; networks provide isolation and discovery
  • The containerd-shim decouples running containers from daemon lifecycle

With the architecture understood, Part 6 will put this knowledge into practice with a comprehensive exploration of the Docker CLI — the primary interface for building, running, inspecting, and debugging containers.

Next in the Series

In Part 6: Docker CLI Mastery, we will explore every essential Docker command — from container lifecycle and image management to inspection, debugging, and system maintenance — building fluency with the tool you'll use daily.