containerd is the engine that actually runs containers for Docker.

Let’s see it in action. Imagine you’ve got a Dockerfile like this:

FROM alpine:latest
CMD ["echo", "Hello from container!"]

When you run docker build -t my-alpine ., Docker doesn’t directly create and start the container. Instead, it orchestrates a series of calls to containerd. Here’s a simplified look at what happens under the hood, using ctr, the containerd CLI, to show what Docker is indirectly commanding:

First, Docker needs to pull the alpine:latest image. It tells containerd to fetch it.

# This is what Docker is *telling* containerd to do
ctr image pull docker.io/library/alpine:latest

ctr will then communicate with its configured image service, which might involve fetching manifests, layers, and ultimately storing them in containerd’s own image store (typically /var/lib/containerd/image/overlayfs/).

Next, Docker needs to create a container from that image. It specifies the image, a name for the container, and its root filesystem.

# Docker tells containerd to create a container
ctr content fetch docker.io/library/alpine:latest # (This happens implicitly with pull)
ctr image mount docker.io/library/alpine:latest # Mounts the image layers
ctr content get docker.io/library/alpine:latest # Gets the image config
ctr image export docker.io/library/alpine:latest /tmp/alpine-rootfs # Exports rootfs (simplified)

# Now, create the container
ctr c create \
  -n my-alpine-container \
  -i \
  -t \
  --image-ref docker.io/library/alpine:latest \
  --rootfs /tmp/alpine-rootfs # Path to the exported rootfs

This ctr c create command tells containerd to set up the container’s filesystem, namespaces, and other configurations. containerd creates a new container object within its internal state, referencing the image layers.

Finally, Docker tells containerd to start the container. This is where the actual execution begins.

# Docker tells containerd to start the container
ctr c start my-alpine-container

ctr c start instructs containerd’s task manager to create a new process, set up the necessary OCI runtime (like runc), and execute the container’s command (echo "Hello from container!" in this case) within its isolated environment. The output of this command is then piped back to Docker, which makes it appear as if Docker itself is running the command.

The real magic here is that Docker acts as a high-level orchestrator, managing the lifecycle of containers, their networks, volumes, and build processes. But when it comes to the low-level act of creating and running the actual isolated process, it delegates that responsibility to containerd. containerd, in turn, uses a lower-level runtime like runc (which implements the Open Container Initiative’s runtime specification) to spin up the isolated process.

This separation of concerns is a core design principle. Docker handles the user experience, image management (via its daemon), networking, and storage plugins. containerd focuses purely on managing container lifecycles: image transfer, storage, execution, and supervision. It provides a stable, well-defined API for these operations, which Docker consumes.

The most surprising thing is how much of Docker’s core functionality has been abstracted away into containerd over time. What used to be tightly coupled within the dockerd binary is now a distinct service with its own API. This allows for independent development and has enabled other container runtimes and orchestrators to integrate with containerd directly, bypassing the Docker daemon for certain operations if desired. For example, Kubernetes can use containerd as a Container Runtime Interface (CRI) implementation, effectively talking directly to containerd to manage pods, without needing the Docker daemon at all.

This modularity means that when you run docker run, you’re not just executing a Docker command; you’re initiating a chain of communication from the Docker CLI, to the Docker daemon, which then instructs containerd, which finally calls out to a runtime like runc to create and run your container.

The next step is understanding how containerd manages the networking for these containers, often through plugins like CNI.

Want structured learning?

Take the full Docker course →