Docker Architecture: Under the Hood

[!IMPORTANT] Docker is not a monolithic application. It is a Client-Server system composed of multiple specialized binaries.

1. The Big Picture

When you run docker run nginx, you are not actually running a container. You are talking to a daemon that runs the container for you.

Interactive: The Request Lifecycle Visualizer

Click “Send Request” to trace the journey of a docker run command through the system components.

Docker Client
CLI / REST API
dockerd
API Server & Orchestrator
containerd
Image Pull & Execution Manager
runc
OS Interface (Namespaces)
Waiting for command...

2. The Components Explained

1. The Client (docker)

The Docker CLI is just a REST client. It does nothing related to running containers itself. It converts your commands into HTTP requests and sends them to the Docker Daemon.

2. The Daemon (dockerd)

The persistent background process.

  • API Server: Listens on /var/run/docker.sock (Unix socket) or a TCP port.
  • Orchestrator: Manages higher-level objects like Networks, Volumes, and Swarm mode.
  • It does not manage container execution anymore. It delegates that to containerd.

3. containerd

A CNCF graduated project. It is an industry-standard container runtime manager.

  • Job: Manages the complete container lifecycle (Image pull, storage, execution, supervision).
  • Why split it out?: So that other tools (like Kubernetes) can use containerd directly without the overhead of the full Docker daemon.

4. runc

The low-level OCI (Open Container Initiative) runtime.

  • Job: Talk to the Linux Kernel to create namespaces and cgroups.
  • It is a short-lived process. It spawns the container and then exits.

5. containerd-shim

If runc exits, who watches the container? The shim.

  • It sits between containerd and the container process.
  • It keeps the standard input/output (STDIO) streams open.
  • It reports the exit status back to containerd.
  • Crucial Feature: It allows “daemonless containers”. You can restart the Docker daemon (upgrade it) without killing your running containers because the shim owns them, not the daemon.

3. Interacting with the API (Go Example)

Since Docker is just an API server, we can talk to it programmatically using Go. This is how tools like Kubernetes or CI pipelines control containers.

We use the official SDK: go get github.com/docker/docker/client

package main

import (
	"context"
	"fmt"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/client"
)

func main() {
	// 1. Create a client connected to the local docker socket
	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	if err != nil {
		panic(err)
	}

	// 2. List running containers (equivalent to `docker ps`)
	containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{})
	if err != nil {
		panic(err)
	}

	for _, container := range containers {
		fmt.Printf("ID: %s | Image: %s | State: %s\n",
            container.ID[:10], container.Image, container.State)
	}
}

[!TIP] This code bypasses the docker CLI entirely and talks directly to dockerd via the Unix socket.

Why does this matter?

Understanding this separation explains why:

  1. Security: The daemon runs as root, so access to the socket is equivalent to root access.
  2. Stability: You can restart Docker without killing containers (thanks to shim).
  3. Performance: containerd is highly optimized for starting containers quickly.