Explain the container runtime architecture from Docker CLI down to the Linux kernel. What are the roles of dockerd, containerd, runc, and the OCI specification? What happens at each layer when you run 'docker run nginx'?
Quick Answer
Docker CLI sends API request to dockerd (Docker daemon), which delegates to containerd (container lifecycle manager), which calls runc (OCI runtime) to set up Linux namespaces, cgroups, and pivot_root to create the isolated container process. The OCI spec defines the container image format and runtime behavior.
Detailed Answer
Complete Execution Flow of 'docker run nginx'
Layer 1 - Docker CLI
The docker binary sends a REST API call to dockerd's socket (/var/run/docker.sock): POST /containers/create followed by POST /containers/{id}/start. The CLI is just an HTTP client.
Layer 2 - dockerd (Docker Daemon)
dockerd handles image management, networking (bridge, overlay), volumes, and build operations. It pulls the nginx image if not cached locally, creates a container configuration (mounts, env vars, networking), and delegates to containerd via gRPC.
Layer 3 - containerd
containerd is the container lifecycle manager. It manages image content (content store), snapshots (filesystem layers), and container execution. It creates an OCI bundle (rootfs + config.json) and calls the OCI runtime. containerd uses shim processes (containerd-shim-runc-v2) to decouple container lifecycle from the containerd process - containers survive containerd restarts.
Layer 4 - runc
runc is the OCI reference runtime. It reads the OCI bundle (config.json specifying namespaces, cgroups, capabilities, seccomp profiles) and makes Linux syscalls to create the container: 1. clone() with CLONE_NEWPID, CLONE_NEWNET, CLONE_NEWNS, CLONE_NEWIPC, CLONE_NEWUTS 2. Set up cgroups (cpu, memory, blkio limits) 3. pivot_root() to change the root filesystem 4. Drop capabilities, apply seccomp filter 5. exec() the container entrypoint (nginx)
Layer 5 - Linux Kernel
Namespaces provide isolation (PID, network, mount, IPC, UTS, user). Cgroups provide resource limits. Seccomp restricts syscalls. AppArmor/SELinux provide MAC security.
Code Example
# Trace the full container creation path
# 1. Docker CLI -> dockerd API
docker run --rm -d --name web nginx:alpine
curl --unix-socket /var/run/docker.sock http://localhost/containers/json
# 2. See containerd's view
ctr -n moby containers list
ctr -n moby tasks list
# 3. See the OCI bundle that runc receives
ls /run/containerd/io.containerd.runtime.v2.task/moby/<container-id>/
# Contains: config.json, rootfs/, work/
# 4. Inspect the OCI config.json
jq '.process.capabilities' /run/containerd/io.containerd.runtime.v2.task/moby/<id>/config.json
jq '.linux.namespaces' /run/containerd/io.containerd.runtime.v2.task/moby/<id>/config.json
# 5. See kernel namespaces for the container process
docker inspect --format '{{.State.Pid}}' web
ls -la /proc/<PID>/ns/ # Shows namespace inodes
# 6. See cgroup limits
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/memory.max
cat /sys/fs/cgroup/system.slice/docker-<id>.scope/cpu.max
# 7. Skip Docker entirely - run container with runc
mkdir -p bundle/rootfs
docker export $(docker create nginx:alpine) | tar -C bundle/rootfs -xf -
runc spec --bundle ./bundle
runc run --bundle ./bundle my-containerInterview Tip
This question is asked at Google, Meta, and Netflix to test whether you understand containers beyond just 'docker run'. The key insight is that Docker is just one client - Kubernetes uses containerd directly (no dockerd). Know the shim process model and why containers survive daemon restarts. Being able to run containers with runc directly shows deep understanding.