Skip to main content

Understanding Docker Architecture - Daemon, Client, and Container Runtime

Learn how the Docker daemon, CLI client, containerd runtime, and runc work together to create and manage containers on a Linux host.

44 Terms

Overview and What You Will Learn

When you type docker run nginx and a container appears in seconds, it looks like magic. It is not. That single command travels through four distinct software layers before a container process starts on your machine. In this guide you will learn exactly what those four layers are, what each one does, and why Docker was designed this way. You will also understand the Docker socket — the most important and most dangerous file on any Docker host — and what happens if it is exposed incorrectly.

By the end of this guide you will be able to explain how Docker works at the architecture level, understand why Docker Desktop behaves differently from Docker Engine on Linux, and know how to configure the Docker daemon for production use.

Why This Matters in Production

At Swiggy, every EC2 instance running containers has a Docker daemon running on it. When containers fail to start, when builds mysteriously hang, or when a security team raises a concern about the Docker socket — engineers who understand the architecture can diagnose and fix these problems in minutes. Engineers who only know docker run commands are stuck waiting for someone else to help.

Understanding the Docker daemon also explains why Kubernetes deprecated Docker in version 1.24 and switched to containerd directly — a change that confused thousands of engineers who did not understand the architecture underneath.

Core Principles

Docker's architecture has four layers. Each layer has a single responsibility and communicates with the next layer through a well-defined interface.

Bash
+------------------------------------------+
| docker CLI |
| (the command you type in your terminal) |
| Sends REST API calls to the daemon |
+------------------------------------------+
|
| REST API over Unix socket
| /var/run/docker.sock
v
+------------------------------------------+
| dockerd (Docker Daemon) |
| The background service managing objects |
| Handles: images, containers, networks, |
| volumes, builds, and the API |
+------------------------------------------+
|
| gRPC calls
v
+------------------------------------------+
| containerd |
| Container lifecycle management |
| Pulls images, manages snapshots, |
| starts and stops container processes |
+------------------------------------------+
|
| OCI runtime spec
v
+------------------------------------------+
| runc |
| The actual container runner |
| Creates Linux namespaces and cgroups |
| Starts the container process |
| Exits immediately after container starts |
+------------------------------------------+

This separation of concerns is intentional. containerd and runc are independent projects that can be used without Docker. Kubernetes uses containerd directly — skipping dockerd entirely — which is why Docker was deprecated as a Kubernetes runtime.

Detailed Step-by-Step Practical Lab

Milestone 1: Understanding the Docker Daemon

The Docker daemon (dockerd) is a background service that starts when your system boots and runs continuously. It listens for API requests and manages all Docker objects on the host.

Bash
sudo systemctl status docker
# Output you want to see:
# docker.service - Docker Application Container Engine
# Loaded: loaded (/lib/systemd/system/docker.service; enabled)
# Active: active (running) since Mon 2024-01-15 09:00:00 IST
# If it is not running, start it
sudo systemctl start docker
# Check the daemon version and runtime info
docker info
# Output includes:
# Server Version: 24.0.7
# Storage Driver: overlay2
# Logging Driver: json-file
# Cgroup Driver: systemd
# Runtimes: runc
# containerd version: v1.7.8
# View daemon logs to diagnose startup problems
sudo journalctl -u docker.service -f
# Follow live daemon logs — useful when builds fail or containers won't start

The daemon configuration file lives at /etc/docker/daemon.json. This is where you configure production settings:

JSON
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
},
"storage-driver": "overlay2",
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 64000,
"Soft": 64000
}
},
"live-restore": true
}

The live-restore: true setting is critical for production — it tells the daemon to keep containers running if the daemon itself is restarted or upgraded. Without it, every daemon restart kills all running containers.

Bash
# After editing daemon.json, reload without stopping containers
sudo systemctl reload docker
# Or do a full restart (kills containers unless live-restore is set)
sudo systemctl restart docker
Milestone 2: Understanding the Docker Socket

The Docker socket is a Unix socket file at /var/run/docker.sock. The Docker CLI sends REST API calls to the daemon through this socket. Whoever can read and write to this socket has full control over Docker — which means full control over the host.

Bash
# Check the socket file and its permissions
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker 0 Jan 15 09:00 /var/run/docker.sock
# Owner: root, Group: docker
# Only root and members of the docker group can use Docker
# See what API calls your docker commands actually make
# Run this in one terminal:
sudo socat -v UNIX-LISTEN:/tmp/docker-debug.sock,fork \
UNIX-CONNECT:/var/run/docker.sock &
# Then in another terminal:
DOCKER_HOST=unix:///tmp/docker-debug.sock docker ps
# Watch the raw API calls appear in the first terminal
# You will see: GET /v1.43/containers/json HTTP/1.1

The Docker socket is the most common Docker security vulnerability. Mounting it inside a container gives that container root access to the host:

Bash
# NEVER DO THIS IN PRODUCTION
docker run -v /var/run/docker.sock:/var/run/docker.sock \
some-image
# Any process inside that container can now:
# - Run new privileged containers
# - Mount the host filesystem
# - Read all secrets from other containers
# - Escalate to full root on the host
# If you need Docker inside Docker for CI, use:
# - Docker-in-Docker (dind) with --privileged flag in a controlled environment
# - kaniko for building images without Docker socket access
# - buildah as a Docker socket free alternative
Milestone 3: Understanding containerd and runc

containerd was split out of Docker in 2016 and donated to the CNCF. It handles the actual container lifecycle — pulling images, managing filesystem snapshots, and starting container processes. The Docker daemon talks to containerd through a gRPC socket.

Bash
# containerd socket (separate from Docker socket)
ls -la /run/containerd/containerd.sock
# Check containerd status
sudo systemctl status containerd
# List containers directly through containerd (bypasses Docker)
# Useful for debugging when Docker daemon is having issues
sudo ctr containers list
sudo ctr tasks list
# containerd uses namespaces — Docker containers are in the 'moby' namespace
sudo ctr --namespace moby containers list

runc is even simpler — it reads an OCI bundle (a filesystem and a config.json file) and creates the Linux namespaces and cgroups that isolate the container. It then executes the container process and exits immediately. runc does not run continuously — it is a one-shot binary that sets up isolation and hands off.

Bash
# See runc in action during a container start
# In one terminal, watch for runc processes:
watch -n 0.1 'ps aux | grep runc'
# In another terminal, start a container:
docker run -d nginx
# You will briefly see:
# root /usr/bin/runc --root /var/run/docker/runtime-runc/moby --log ...
# It appears for less than a second — runc starts the container and exits
Milestone 4: Tracing a docker run Command End-to-End

Here is the complete sequence of what happens when you run docker run nginx:

Bash
Step 1: docker CLI
You type: docker run nginx
CLI parses: image=nginx, no command specified, no flags
CLI calls: POST /v1.43/containers/create to Docker daemon
Step 2: Docker daemon receives the request
Checks: does nginx image exist locally?
If not: pulls from registry (calls containerd to pull)
Creates: container configuration object
Calls: containerd to start the container
Step 3: containerd receives the request
Prepares: container filesystem snapshot using OverlayFS
Creates: OCI bundle (rootfs + config.json)
Calls: runc to start the container process
Step 4: runc executes
Creates: new PID namespace (container gets PID 1)
Creates: new network namespace (container gets its own network)
Creates: new mount namespace (container gets its own filesystem view)
Creates: new UTS namespace (container gets its own hostname)
Sets up: cgroups for CPU and memory limits
Executes: the container's entrypoint process (nginx)
Exits: runc process exits, container keeps running
Step 5: containerd monitors the container process
Watches: container process health
Reports: container status back to Docker daemon
Step 6: Docker daemon updates its state
Records: container is now running
Returns: container ID to CLI
CLI prints: container ID to your terminal
Bash
# Verify this sequence yourself
docker run --name=trace-test -d nginx
# Check which process is actually running
docker top trace-test
# UID PID PPID CMD
# root 12345 1 nginx: master process nginx -g daemon off
# That PID is on the HOST — containers are just processes with isolation
ps aux | grep nginx
# You will see the same PID — containers are not separate machines
Milestone 5: Docker Desktop vs Docker Engine

Docker Engine is the native Linux installation — the daemon runs directly on the Linux host. Docker Desktop (on macOS and Windows) runs a lightweight Linux VM and runs Docker Engine inside it. This explains several behaviours that confuse engineers:

Bash
# On Linux — Docker runs natively
# /var/run/docker.sock is on the actual host filesystem
# On macOS — Docker Desktop runs a hidden Linux VM
# /var/run/docker.sock is actually inside the VM
# Port bindings (-p 8080:80) work through VM port forwarding
# Volume mounts (-v /home/user/code:/app) go through VM filesystem sharing
# This is why volume performance is slower on macOS than Linux
# Check which context you are using
docker context ls
# NAME DESCRIPTION DOCKER ENDPOINT
# default Current DOCKER_HOST unix:///var/run/docker.sock
# desktop Docker Desktop ...
# Switch contexts if you have multiple Docker environments
docker context use desktop

Common Mistakes

Mistake Why It Is Dangerous Fix
Mounting /var/run/docker.sock into containers Full host root access from inside container Use kaniko or buildah for CI builds instead
Running daemon without live-restore: true All containers die when daemon restarts Add "live-restore": true to daemon.json
Not setting log rotation in daemon.json Logs fill the disk over days/weeks Always set max-size and max-file
Adding users to the docker group without thinking Docker group = root equivalent access Only add users who genuinely need Docker access
Using Docker Desktop file mounts for database volumes Slow I/O causes database performance issues Use named Docker volumes for databases on macOS

Troubleshooting Reference

Problem Symptom Diagnostic Command Fix
Daemon not running Cannot connect to Docker daemon systemctl status docker systemctl start docker
Permission denied Got permission denied while trying to connect groups Add user to docker group: usermod -aG docker $USER
Daemon config invalid Daemon fails to start after editing daemon.json dockerd --validate Fix JSON syntax error in daemon.json
Socket not found docker: /var/run/docker.sock: no such file ls /var/run/docker.sock Restart Docker daemon
containerd issues Containers won't start despite daemon running systemctl status containerd systemctl restart containerd
High disk usage Disk filling up on build machines docker system df docker system prune -f
Tip

On production Linux hosts, always add "live-restore": true to /etc/docker/daemon.json before running any workloads. Without it, every Docker daemon upgrade or crash kills all running containers. With it, containers keep running while the daemon restarts — zero downtime for daemon maintenance.

Remember

The Docker socket at /var/run/docker.sock is functionally equivalent to root access on the host. Treat it with the same care as an SSH private key. Never mount it into untrusted containers, never expose it over TCP without TLS, and audit which processes have access to it on every production host.

Common Mistake

Thinking that containers are separate virtual machines. Containers are Linux processes running on the host kernel with isolation provided by namespaces and cgroups. When you run ps aux on the host, you can see all container processes with their real PIDs. This is why a compromised container can be a serious security risk — it is running on the same kernel as everything else on the host.

Security

The members of the docker group on a Linux host have effective root access. Running docker run -v /:/host alpine chroot /host gives any docker group member a root shell on the host filesystem. Never add untrusted users to the docker group. On multi-tenant servers, use rootless Docker or Podman instead, which do not require a privileged daemon.

Resources

Explore More in Docker Fundamentals and Container Lifecycle

All 3 Topics

Frequently Asked Questions

Is Understanding Docker Architecture - Daemon, Client, and Container Runtime free to learn on DevOps Network?

Yes - this topic, like everything on DevOps Network, is 100% free with no paywall or sign-up gate.

What does the Understanding Docker Architecture - Daemon, Client, and Container Runtime topic cover?

Learn how the Docker daemon, CLI client, containerd runtime, and runc work together to create and manage containers on a Linux host.