unkletayo
2

What Happens When You Type docker run? From CLI to containerd to runc

Following the chain of processes that a single docker run command sets off.

Adetayo Akinsanya 2026-09-29 14 min read

  • Docker
  • Architecture
  • containerd
  • runc
  • Linux
  • DevOps

Part 2 of the series. Read What Docker Actually Adds to Linux Isolation Primitives (Part 1) first if you have not.

In this article (8 sections)
  1. The Architecture of the Process Chain
  2. 1. Docker CLI (/usr/bin/docker)
  3. 2. Docker Daemon (dockerd)
  4. 3. containerd
  5. 4. containerd-shim
  6. 5. runc
  7. Inspecting the Live Process Tree
  8. Summary & Next Steps

Before you start: you should know what a container and an image are (part 1 of this series) and be able to type a command into a terminal. You do not need to know anything about operating systems. Every unfamiliar term is explained where it first appears.

You log into a production host (the computer, often a remote Linux server, that runs your containers) and execute a simple command:

docker run -d --name web-server -p 80:80 nginx

The flags are small instructions to Docker. -d runs the container in the background so your terminal is free again. --name web-server gives the container a name you can use later. -p 80:80 forwards port 80 on the host to port 80 inside the container (a port is a numbered doorway for network traffic, and web servers listen on port 80 by default). nginx is the image to run, and Nginx is a widely used web server program.

The command returns a 64-character container ID, and Nginx starts serving web traffic.

Later, an engineer performs a rolling software update on the Docker system daemon:

systemctl restart docker

systemctl is the Linux command that starts, stops and restarts background services, and this one restarts the Docker background program. A daemon is a program that runs in the background with no window, waiting for requests. dockerd is the daemon that does Docker’s work.

While the dockerd daemon restarts, not a single HTTP request to the running Nginx container drops. The web server keeps running uninterrupted.

How does Docker restart its main control daemon without killing active running containers?

To answer this, we must examine the multi-layered process architecture that executes underneath the docker command line interface (CLI, a program you control by typing commands): CLI to dockerd to containerd to containerd-shim to runc. The answer arrives in the process tree section near the end of this article.

An everyday picture helps. Think of a restaurant. The host who takes your order is the CLI. The manager is dockerd, who sets up the table and the bill. The kitchen lead is containerd, who runs each dish from start to finish. A cook assigned to your table is the shim, and the builder who prepares the cooking station and then leaves is runc. If the manager goes home, the cook who is already cooking your dish keeps going. Each name in the chain is a separate program that hands work to the next one, and the rest of the article walks down that chain.


The Architecture of the Process Chain

Modern Docker is not a single monolithic process. It is a collection of modular tools connected via UNIX sockets and gRPC interfaces.

A UNIX socket is a special file that two programs on the same machine use to send messages to each other. gRPC is a standard way for one program to call another program’s functions by sending structured messages. A process is one running program, and every process is started by a parent process.

Read the diagram below from top to bottom. Each box receives a request from the box above it and passes work to the box below it. The paths like /usr/bin/docker show where each program file lives on a Linux disk, and you do not need to memorize them.

[ User Shell ]
      |
      |  docker run nginx
      v
+-------------------------------------------------------------------+
| 1. Docker CLI (/usr/bin/docker)                                   |
+-------------------------------------------------------------------+
      |
      |  HTTP REST API payload over /var/run/docker.sock
      v
+-------------------------------------------------------------------+
| 2. Docker Daemon (/usr/bin/dockerd)                               |
|    - Handles image builds, network bridges, volume management     |
+-------------------------------------------------------------------+
      |
      |  gRPC Requests over /run/containerd/containerd.sock
      v
+-------------------------------------------------------------------+
| 3. containerd Daemon (/usr/bin/containerd)                        |
|    - Manages image snapshots, container lifecycles, content store |
+-------------------------------------------------------------------+
      |
      |  Forks a light daemonless shim process per container
      v
+-------------------------------------------------------------------+
| 4. containerd-shim (/usr/bin/containerd-shim-runc-v2)             |
|    - Holds open stdin/stdout/stderr file descriptors              |
|    - Manages container exit status codes                          |
+-------------------------------------------------------------------+
      |
      |  Executes OCI Bundle
      v
+-------------------------------------------------------------------+
| 5. runc CLI (/usr/bin/runc)                                       |
|    - Sets up Linux namespaces & cgroups via libcontainer          |
|    - Executes entrypoint binary (PID 1)                           |
|    - Replaces itself with that binary (execve), so it is gone     |
+-------------------------------------------------------------------+
      |
      v
+-------------------------------------------------------------------+
| 6. Containerized Process (nginx: master process)                  |
+-------------------------------------------------------------------+

In plain words, the diagram says this:

  1. You type docker run nginx. The CLI turns it into a web-style request and sends it through the socket file /var/run/docker.sock.
  2. dockerd receives the request. It prepares the parts around the container (images, networking, storage) and then asks containerd to run it, using gRPC over /run/containerd/containerd.sock.
  3. containerd manages the container’s life: fetching its files and deciding when it starts and stops. For each container it starts one small helper process, the shim.
  4. The shim stays beside the container. It keeps the container’s input and output connected (stdin is where a program reads input, stdout is where it prints normal output, and stderr is where it prints errors) and records how the container ends. It calls runc to do the actual setup.
  5. runc asks the Linux kernel (the core of the operating system) to build the isolated environment, then turns itself into the container’s program. Namespaces and cgroups, explained in section 5, are how that isolation is built.
  6. The last box is the container’s own program, here the Nginx master process, which is the main Nginx process that starts the others.

The shim’s real file name is containerd-shim-runc-v2. The containerd-shim in the box title and in the process tree later is the same program, shortened. Tools that list processes often show a shortened name, because Linux keeps only the first 15 characters of a process name, and containerd-shim is exactly 15 characters.


1. Docker CLI (/usr/bin/docker)

The docker command is a client-side Go binary. A binary is a ready-to-run program file, Go is the programming language it was written in, and client-side means it only sends requests and does not do the work itself. It performs no container management or namespace creation itself.

When you type docker run, the CLI translates flags (-d, -p 80:80) into an HTTP JSON payload and transmits it across a local UNIX domain socket at /var/run/docker.sock to the Docker Daemon. HTTP is the request and reply format web browsers use, JSON is a text format for structured data such as {"name": "web"}, and a payload is the data being sent. The socket is a file path standing in for a website address, so the request never leaves the machine:

POST /v1.43/containers/create?name=web-server HTTP/1.1
Host: localhost
Content-Type: application/json

{
  "Image": "nginx",
  "ExposedPorts": { "80/tcp": {} },
  "HostConfig": {
    "PortBindings": { "80/tcp": [{ "HostPort": "80" }] }
  }
}

POST means “create something”, and /containers/create names what to create. The v1.43 is the version number of Docker’s API, and the name=web-server part comes from your --name flag. An API (application programming interface) is a set of web-style addresses a program exposes so that other programs can ask it to do things, and a REST API is one built in that style. In the body, 80/tcp means port 80 using TCP, the usual protocol for web traffic.


2. Docker Daemon (dockerd)

The dockerd process acts as the high-level orchestrator for host-level container features. In the restaurant picture, it is the manager who sets up everything around the dish:

  • Build Engine: Translates Dockerfile instructions into image layers. A Dockerfile is a text recipe of steps for building an image, an image is a read-only package of files plus start settings, and a layer is one step’s file changes stacked on the previous ones.
  • Virtual Networking: Allocates IP addresses, updates iptables rules, and sets up virtual bridge interfaces (docker0). An IP address is the numeric address that identifies a machine on a network, and each container gets its own. iptables is Linux’s built-in list of rules for filtering and routing network traffic. A virtual bridge is a software network switch that connects containers to each other and to the host.
  • Volume Management: Mounts host directories or cloud block storage into container paths. Mounting makes a folder or disk appear at a chosen path inside the container, and a host directory is a folder on the real machine.
  • REST API Server: Authenticates requests and exposes endpoints.

Once dockerd completes high-level network and storage preparation, it hands off container execution to containerd.


3. containerd

If dockerd is the manager who takes the customer’s requirements, containerd is the kitchen lead who runs each dish from start to finish.

containerd is an industry-standard, CNCF-graduated container runtime daemon. CNCF is the Cloud Native Computing Foundation, a non-profit that hosts open-source infrastructure projects, and graduated is its highest maturity level. A container runtime is the software that actually starts and manages containers.

While dockerd handles high-level user features, containerd manages low-level container lifecycle execution:

  • Pulling and pushing container images from registries. A registry is a server that stores images for download, and Docker Hub is one.
  • Unpacking image layers into root filesystems using OverlayFS snapshotters. A root filesystem is the folder tree that becomes the container’s /. OverlayFS is a Linux feature that stacks read-only layers into one combined view, and a snapshotter is the containerd component that prepares that view.
  • Managing container execution state (start, stop, pause).

containerd listens on a gRPC socket at /run/containerd/containerd.sock. It can operate completely independent of Docker (for example, Kubernetes uses containerd directly via CRI without dockerd). Kubernetes is software that runs many containers across many machines, and CRI (Container Runtime Interface) is the interface it uses to ask a runtime to start containers.


4. containerd-shim

A shim is a small adapter that sits between two parts. Here it is a tiny babysitter program that stays alive next to one container even if its bosses restart.

When containerd is instructed to start a container, it does not launch the container process directly under its own process tree. Every running program is a process started by a parent process, and together they form a tree. Fork means to create a new child process.

Instead, containerd forks an intermediate process called containerd-shim. The file on disk is named containerd-shim-runc-v2, which is the same program.

Why the Shim Exists:

  1. Daemonless Container Execution: The shim acts as the parent process for the container’s PID 1. If dockerd or containerd crash or undergo system upgrades, the shim keeps running, keeping the container process alive! A PID is the number Linux gives each process, and PID 1 here means the first process inside the container, the one the container lives and dies with. The parent matters because a process is tied to the one that started it. If the container were a direct child of dockerd, stopping dockerd would leave it orphaned, with no parent left to track it. Because the container’s parent is the shim, and the shim is not dockerd, restarting dockerd does not touch the container. This is the answer to the question at the top, and the process tree below shows it.
  2. I/O Streaming: Holds open the standard I/O pipes (stdin, stdout, stderr) so logs are captured even if the daemon restarts. Stdin is where a program reads input, stdout is where it prints normal output, stderr is where it prints errors, and a pipe carries that data between programs. Each of these is a file descriptor, a small number a process uses to refer to an open file or pipe.
  3. Exit Code Reporting: Waits for the container PID 1 process to terminate and reports its exit code back to containerd. An exit code is the number a program returns when it ends, and 0 usually means success.

5. runc

runc is like the builder who sets up a sealed room (the container), moves the program in, and leaves. It does not stay to supervise.

runc is the reference implementation of the Open Container Initiative (OCI) Runtime Specification. A specification is a written rulebook, and a reference implementation is a program that shows how to follow it. OCI is a group that writes shared standards for containers, and part 3 covers it.

When containerd-shim starts, it invokes runc create passing an OCI Bundle (a directory containing a config.json file and a rootfs/ directory). The config.json file holds the container’s settings, and rootfs/ holds the files the container will see as its whole disk.

How runc Creates the Container:

runc translates the settings file into requests to the Linux kernel. Those requests are system calls: the way a program asks the kernel (the core of the operating system) to do something. The (2) after each name is the manual page section number for system calls, and you can ignore it.

  1. Reads config.json to extract namespace settings, cgroup limits, capabilities, and environment variables. A namespace gives a process its own private view of one system resource, such as the process list, the network or the files. A cgroup limits how much CPU or memory a group of processes may use. Capabilities are the individual special privileges a process may hold, and environment variables are named settings handed to a program when it starts.
  2. Invokes Linux system calls (clone(2) or unshare(2)) to create new namespaces (PID, NET, MNT, UTS, IPC). These are the process IDs, the network, the mounts (what is attached where in the file tree), the hostname, and the messaging between processes. clone creates a new process, and unshare moves the calling process into new namespaces.
  3. Configures cgroup resource limits in /sys/fs/cgroup/, a special folder where Linux exposes cgroup settings as files.
  4. Executes pivot_root(2) to lock the process into rootfs/. This makes rootfs/ appear as the whole filesystem, so the process cannot see the host’s files.
  5. Executes execve(2) to replace its own process memory with the target container binary (e.g., nginx). execve swaps the program running in a process for another program, inside the same process.

Crucially, execve(2) does not start a second process. The runc process replaces itself with Nginx, so no runc is left running. Because the same process simply becomes Nginx, runc uses no memory or CPU of its own while the container runs.


Inspecting the Live Process Tree

pstree prints running programs as a family tree. In the command below, pgrep containerd finds containerd’s process number and $(...) pastes that number into the command. You can observe this process chain directly on any Linux host using pstree:

$ pstree -plu $(pgrep containerd)
containerd(1042)─┬─containerd-shim(8920)───nginx(8945)───nginx(8990)
                 └─{containerd}(1043)

The shim appears here as containerd-shim, the shortened form of containerd-shim-runc-v2. The line {containerd}(1043) is a thread of containerd, meaning a separate strand of work inside the same process. In the output, the number in parentheses is each process’s ID.

Notice the process tree hierarchy:

  • containerd (PID 1042) is the parent of containerd-shim (PID 8920).
  • containerd-shim is the parent of the nginx master process (PID 8945).
  • runc is nowhere in the process tree because it replaced itself with Nginx after setup!
  • dockerd is absent from the container process parentage, which is why restarting dockerd does not disrupt Nginx. It handed the job to containerd and only talks to it by message, so it is not a parent of anything here.

Summary & Next Steps

Docker’s modular toolchain separates high-level developer APIs from low-level kernel isolation execution:

  • docker CLI converts terminal commands into REST API requests sent over /var/run/docker.sock.
  • dockerd orchestrates networks, volumes, and API requests.
  • containerd manages image snapshots and container lifecycles via gRPC.
  • containerd-shim keeps container processes alive independently of daemon restarts.
  • runc executes system calls to set up namespaces/cgroups and then replaces itself with the container process.

In the next article, we examine The Open Container Initiative (OCI): Image Specification and Runtime Specification.

Questions or corrections

Found a mistake or have a question about this breakdown? Reach out directly.

Reply on X (@unkletayor) Email feedback