01-foundations-containers

Inside docker run: CLI, Daemon, containerd and runc

Follow the chain between pressing Enter on docker run and a process existing — and learn why Docker's socket is a host-level trust decision.

September 13, 2026
dockerarchitecturedockerdcontainerdruncrootlessdocker-socket

Why Bother With the Plumbing?

It's tempting to treat docker as a single program that runs containers. It isn't, and the difference shows up in practical ways:

  • Why does Docker need a background service running at all, when ls doesn't?
  • Why is adding your user to the docker group quietly equivalent to giving them root?
  • Why can a CI job "have Docker" without Docker Desktop, and why is -v /var/run/docker.sock:/var/run/docker.sock the line reviewers flag hardest?
  • Why does Kubernetes not use Docker, while still happily running images you built with it?

All four answers are the same answer: docker is a client, and the real work happens somewhere else.

The Chain

The docker CLI is a thin client. It takes your command, turns it into an HTTP request against Docker's REST API, and prints the response. It creates nothing itself.

The socket is where that request goes — on Linux, a Unix socket at /var/run/docker.sock. Hold that thought.

dockerd is the daemon that receives the request. It owns the things you think of as "Docker features": building images, managing networks and volumes, and the higher-level orchestration of it all. For actually running containers, it delegates.

containerd manages the container lifecycle and image storage — pulling images, unpacking layers, starting and stopping containers. It's a CNCF project in its own right, used far beyond Docker.

containerd-shim is the small process that stands between containerd and your container. It launches the low-level runtime, then stays alive as the container's parent, holding its I/O streams open and reporting its exit status. Because the shim is the parent rather than containerd, containerd can restart without killing your running containers.

runc does the actual creation: it applies the namespaces and cgroups from the previous guide, sets up the root filesystem, and executes your process. Then runc exits. It is not a long-running supervisor — it's a setup tool that runs once per container start.

💡

Note who does what at the bottom of the chain. runc is the reference implementation of the OCI runtime specification — the same standard from the last guide. Swapping it for another OCI-compliant runtime (gVisor for stronger isolation, or Kata Containers for lightweight VMs) is a configuration change, not a rewrite, precisely because that boundary is standardised.

The Daemon Is Why the Socket Is Dangerous

Here's the consequence that matters most in code review.

dockerd runs as root. It has to — creating namespaces, configuring networks and mounting filesystems are privileged operations. And it will do whatever the API asks it to.

So anyone who can talk to the Docker socket can ask the daemon to start a container that mounts the host's entire filesystem at /host, running as root, and then read or modify anything on the machine. No exploit required — that's the API working exactly as designed.

Two practical consequences follow:

  • Adding a user to the docker group grants root-equivalent access to the host. It's a convenience with a security cost, and it should be a deliberate decision rather than a step you copy from a setup guide.
  • Mounting /var/run/docker.sock into a container gives that container control of the host. This pattern is common — CI runners and tools that need to build images do it — but the container is no longer meaningfully isolated. Treat it as running on the host.
🚨

"Docker-in-Docker" via a mounted socket isn't Docker inside your container at all — it's your container driving the host's daemon. Anything it builds or runs lands on the host, as root. If you see that volume mount, make sure it's deliberate.

Rootless mode is the structural answer: it runs the entire daemon as an unprivileged user, using user namespaces so that root inside a container maps to a non-root user outside. It carries some limitations — privileged ports and certain storage and network features behave differently — but it removes the root-daemon-as-a-single-point-of-compromise problem entirely.

Check yourself

A CI pipeline runs builds inside a container that has /var/run/docker.sock mounted in. A dependency in that build is compromised and runs arbitrary code. What's the realistic blast radius?

Two Different "Restarts"

A distinction worth getting right, because the layers behave differently:

  • containerd restarting does not stop your containers. The shim is the parent process and keeps supervising; containerd reattaches when it returns.
  • dockerd terminating does stop your containers by default. Docker offers a live-restore option that keeps them running across daemon downtime, but it is off unless you turn it on, and it applies to patch upgrades rather than major version jumps.

So "the shim keeps containers alive" is true at the containerd layer, and not something to assume about the Docker daemon above it.

Where Kubernetes Fits

This chain also settles the Docker-and-Kubernetes confusion for good.

Kubernetes needs to start containers on each node, and it talks to a runtime through the Container Runtime Interface (CRI). containerd and CRI-O implement CRI directly. Docker Engine never did — it needed an adapter called dockershim, which Kubernetes maintained and then removed in v1.24.

Look at the diagram again and notice how little is lost: Kubernetes enters the chain at containerd and goes down from there. The parts it skips — dockerd and the docker CLI — are the developer-experience layer, not the container-running layer.

Docker remains an excellent tool for building images — and as the previous guide established, those images are OCI artifacts that containerd runs without modification. Building and running are simply two different jobs.

Check yourself

Your team builds images with docker build in CI and deploys them to a Kubernetes cluster running containerd. Someone proposes switching the build step to a different tool 'because Kubernetes doesn't use Docker'. Is the reasoning sound?

One Modern Detail: the containerd Image Store

Docker Engine 29.0 and later use the containerd image store by default on fresh installations, replacing the older graph-driver approach to storing image layers. Systems upgraded from earlier versions keep the legacy driver until it's enabled explicitly.

For everyday work your commands don't change. What it unlocks is worth knowing: building multi-platform images locally, holding images for several architectures at once, and support for build attestations — all of which come up later in this roadmap.

⚠️

One sharp edge during a migration: switching storage backends temporarily hides images and containers created under the other one. They aren't deleted — the data is still on disk — but docker images will look alarmingly empty. If that happens after an upgrade, you're looking at the wrong store, not a lost library.

What's Next

You know what a container is and what creates one. The final guide of this phase gets hands-on: the container lifecycle, the commands you'll use daily, what exit codes are telling you, and how to use docker inspect to verify what a container actually received — rather than what a command or an AI assistant claimed it would.