From Docker to containerd: A Short History of Kubernetes Runtimes
Why Kubernetes stopped talking to Docker directly, and what containerd actually is in relation to it.
Older Kubernetes tutorials assume Docker is the container runtime. Current ones assume containerd. Neither is wrong, they’re describing different points in Kubernetes’ history, and understanding what changed clears up most of the confusion around ctr, crictl, and why docker ps stopped showing you anything useful on a cluster node.
Docker came first, and Kubernetes was built around it
Docker made containers usable: a CLI, an image format, a daemon that ran everything. Early Kubernetes was built specifically to orchestrate Docker containers, and the two were tightly coupled. As Kubernetes grew, other runtimes (rkt, among others) wanted to integrate too, which meant Kubernetes needed a way to work with more than one runtime instead of being hard-wired to Docker’s API.
CRI and OCI: standardizing the interface
Kubernetes’ answer was the Container Runtime Interface (CRI), a gRPC API the kubelet uses to talk to whatever runtime is installed, without needing runtime-specific code for each one. Any runtime that implements CRI can plug into Kubernetes.
CRI itself leans on standards from the Open Container Initiative (OCI), a spec Docker helped establish precisely so the ecosystem wouldn’t depend on one vendor’s implementation:
- The image spec defines how a container image is structured and addressed, so an image built by one tool can be run by another.
- The runtime spec defines how a container should actually be created and run on a host, down to namespaces, mounts, and process configuration.
- The distribution spec (added later) defines how images are pushed and pulled from a registry.
Between them, CRI and OCI mean a runtime and an orchestrator no longer have to be the same product, or even from the same vendor.
Docker predates CRI, so Kubernetes had to bridge it
Docker wasn’t built to speak CRI, since CRI didn’t exist yet when Docker was created, and Docker remained the dominant runtime regardless. Kubernetes’ workaround was dockershim: a compatibility layer translating CRI calls into Docker’s own API, letting the kubelet keep working with Docker without Docker itself changing.
containerd was already inside Docker
Docker was never just a runtime. It bundled a CLI, a build system, volume and network management, authentication, and, underneath all of that, the piece that actually ran containers: a daemon called containerd, sitting on top of the low-level runtime runc. containerd was factored out as its own project and donated to the CNCF, where it graduated in 2019, and unlike Docker’s user-facing tooling, it speaks CRI natively. That’s the whole relationship in one sentence: containerd is the runtime component that was always inside Docker, now available, and CRI-compatible, on its own.
dockershim’s removal
Maintaining a translation layer for one runtime, when every other runtime just spoke CRI directly, was ongoing overhead for diminishing benefit. Kubernetes removed dockershim in the 1.24 release (2022), and with it, direct support for running Docker as a cluster’s container runtime.
This did not break existing images. Docker built images to the OCI image spec, so anything already sitting in a registry runs on containerd exactly as it did before. What stopped working was using the Docker Engine itself as the thing actually running containers on a node; clusters needed to move to a CRI-native runtime such as containerd or CRI-O.
What this means when you’re on a node today
docker ps on a modern node either doesn’t exist or shows nothing relevant, because Docker isn’t what’s running your containers anymore. The tools for that are crictl, ctr, and nerdctl, and the next guide in this series covers when to reach for each. The container images themselves, the actual application layers, are unaffected by any of this history; what changed is entirely in the machinery underneath.