$ emrebener
home blogs infrastructure & operations kubernetes from dockershim to cri-dockerd: what the cri actually changed

From dockershim to cri-dockerd: what the CRI actually changed

author: emre bener read time: 6 min about: container runtime interface, kubernetes, containerd
published: updated: mentions: docker, cri-o, runc, open container initiative, mirantis, cloud native computing foundation

1. What “Kubernetes dropped Docker” actually meant

In late 2020, Kubernetes 1.20 announced it was deprecating dockershim, and a lot of people read that as Kubernetes removing Docker support. Kubernetes never removed Docker support; it removed a built-in adapter for one container runtime. Images built with Docker kept working, running clusters kept running, and you only had to point the kubelet at a different runtime.

The confusion came from “Docker” meaning several things at once: a CLI, an image format, a build tool, a daemon, and the low-level runtime underneath all of it. Kubernetes only ever used a sliver of that, and CRI is the line it drew around the sliver. The dockershim removal in 1.24 was the end of a migration that had been underway since 2016, not a sudden divorce.

2. The in-tree era: runtimes compiled into the kubelet

Before CRI, the kubelet talked to container runtimes through code compiled directly into it. The first integration was Docker: the kubelet had a Docker-specific manager that called Docker’s API to pull images, create containers, and tear them down. There was no abstraction. Docker was simply what Kubernetes ran on.

Then CoreOS’s rkt got the same treatment. A second runtime meant a second in-tree integration, maintained and tested inside the Kubernetes codebase, and shipped on the kubelet’s release cadence. The pattern didn’t scale. Every new runtime would mean more runtime-specific code in core Kubernetes, and every runtime’s bugs became the kubelet’s problem. The lesson was the usual one: when you find yourself special-casing the second implementation, what you actually wanted was an interface.

Before CRI: each runtime was compiled into the kubeletkubelet (single binary)DockerManager(in-tree)rktManager(in-tree)Docker daemonrktDocker APIrkt APIA new runtime would mean new in-tree code in the kubelet.Before CRI: each runtime was compiled into the kubeletkubelet (single binary)DockerManager(in-tree)rktManager(in-tree)Docker daemonrktDocker APIrkt APIA new runtime would mean new in-tree code in the kubelet.

3. CRI: the runtime becomes a plugin

CRI (Container Runtime Interface) is a gRPC API the kubelet uses to drive whatever runtime sits below it. It arrived as alpha in Kubernetes 1.5, in December 2016. Instead of compiling runtime support in, the kubelet became a gRPC client, and any runtime that implements the server side can run Kubernetes workloads.

The interface is two services. RuntimeService covers the lifecycle: pods (sandboxes), containers, exec and so on. ImageService covers pulling and listing images. A runtime that implements both is a valid Kubernetes runtime, and the kubelet doesn’t need to know anything else about it. The runtime moved from being baked into Kubernetes to being a swappable component behind a stable contract.

Two runtimes fill that slot directly today. containerd implements CRI natively, and CRI-O is a runtime built from scratch to implement CRI for Kubernetes and nothing else. Both talk to the kubelet through the same interface, with no in-tree code in Kubernetes. Docker did not implement CRI, and that gap is the rest of this story.

After CRI: runtimes are swappable plugins behind one gRPC interfacekubeletCRI (gRPC: RuntimeService + ImageService)containerd(native CRI)CRI-O(native CRI)cri-dockerd(shim/adapter)Docker Enginecontainerd and CRI-O speak CRI directly (CRI-native); Docker Engine needs an adapter.After CRI: runtimes are swappable plugins behind one gRPC interfacekubeletCRI (gRPC: RuntimeService + ImageService)containerd(native CRI)CRI-O(native CRI)cri-dockerd(shim/adapter)Docker Enginecontainerd and CRI-O speak CRI directly (CRI-native); Docker Engine needs an adapter.

4. dockershim: the adapter for a runtime that didn’t speak CRI

dockershim was the kubelet’s built-in adapter that translated CRI calls into Docker API calls. Docker didn’t implement CRI, but it was also by far the most common runtime in production, so the Kubernetes team couldn’t drop it when CRI landed. They wrote a shim instead: when the kubelet issued a CRI request, dockershim turned it into the equivalent Docker API call and translated the response back. From the kubelet’s side, Docker looked like any other CRI runtime.

The problem was ownership. dockershim lived inside the kubelet, which meant the Kubernetes project maintained a translation layer for a third-party product it didn’t control. Every change in Docker’s API was potentially the kubelet’s problem, and the shim was special-cased relative to every other runtime, which spoke CRI directly and needed no in-tree code at all. dockershim was deprecated in Kubernetes 1.20 (December 2020) and removed in 1.24 (May 2022). That removal is the event everyone remembers as “Kubernetes dropped Docker.” It was really Kubernetes dropping a maintenance burden it never wanted to carry.

5. containerd was already inside Docker

Docker Engine doesn’t run containers by itself. Underneath the Docker daemon is containerd, and underneath containerd is runc, the low-level tool that actually creates the Linux container. Docker built containerd, used it internally as its core runtime, and donated it to the CNCF (Cloud Native Computing Foundation) in 2017, where it became an independent project.

So containerd was always a full container runtime in its own right. The only missing piece for Kubernetes was CRI, and early on that lived in a separate cri-containerd daemon running alongside containerd. That daemon was later folded into containerd itself as a built-in CRI plugin, so a single containerd process speaks CRI directly with nothing extra to run. The path is short: kubelet to containerd to runc.

This is why the Docker layer is optional for Kubernetes. The Docker daemon adds the Docker CLI, image building, Docker’s networking and volume model, and the Docker API. Kubernetes uses none of that on the running path: it does its own networking through CNI (Container Network Interface), its own storage through CSI (Container Storage Interface), and its own image pulls through CRI. Strip the Docker daemon away and the containerd underneath still does everything the kubelet actually asked for.

6. cri-dockerd and the redundant call chain

When dockershim left the kubelet, the Docker-as-runtime path didn’t disappear; it moved out of tree into cri-dockerd, a standalone CRI implementation maintained by Mirantis that does what dockershim did: translate CRI into Docker API calls. If you want the kubelet to keep running on Docker Engine after 1.24, cri-dockerd is how.

The catch is what the call path looks like once you draw it out. Through cri-dockerd, starting a single container travels:

kubeletcri-dockerddockerdcontainerdrunc

Drop the Docker layer and point the kubelet at containerd directly, and the same operation is:

kubeletcontainerdrunc

Two hops vanish. cri-dockerd translates CRI into the Docker API and the Docker daemon turns most of that right back into containerd calls, because containerd is what was doing the work the whole time. Unless you specifically need something the Docker daemon provides on the node, those two layers are overhead between the kubelet and the runtime that ends up doing the job.

Two paths to the same runtimeWith cri-dockerd (Docker as the runtime)kubeletcri-dockerddockerdcontainerdrunctwo redundant hopsPoint the kubelet straight at containerdkubeletcontainerdruncsame result, two hops shorterThose two extra layers translate CRI into Docker calls that containerd handles anyway.Two paths to the same runtimeWith cri-dockerd (Docker as the runtime)kubeletcri-dockerddockerdcontainerdrunctwo redundant hopsPoint the kubelet straight at containerdkubeletcontainerdruncsame result, two hops shorterThose two extra layers translate CRI into Docker calls that containerd handles anyway.

7. When cri-dockerd is the right choice

For most clusters, run containerd or CRI-O and skip the Docker layer entirely. Both are CRI-native CNCF projects, and both give the kubelet what it needs with no translation shim in the path. That’s the default a new cluster should reach for.

cri-dockerd earns its place in a narrower set of cases. If you have node-level tooling that talks to the Docker socket directly, depend on a Docker-specific feature on the host, or work under a policy that still mandates Docker Engine, cri-dockerd keeps that running without forcing a rewrite. Mirantis maintains it precisely so those environments have a supported path. It’s a compatibility option, not a default.

Images built with Docker run unchanged on containerd and CRI-O. Docker builds OCI (Open Container Initiative) images, OCI is the format every CRI runtime expects, and the kubelet never cared which tool produced the image. Switching the node runtime doesn’t touch your images, your registries, or your Dockerfiles. The visible difference is operational: docker ps on the node becomes crictl ps or a nerdctl command, because the Docker CLI isn’t installed there anymore.

8. What CRI actually changed

CRI didn’t remove Docker from Kubernetes. It removed Docker’s special status. Before CRI, Docker was the runtime, wired into the kubelet by hand. After CRI, it was one implementation behind an interface, and the in-tree shim that kept it privileged became dead weight the Kubernetes team eventually cut.

The same instinct shows up on both sides of that boundary. cri-containerd folded into containerd as a built-in plugin; dockershim moved out of the kubelet into cri-dockerd; the runtime everyone converged on, containerd, was the layer doing the real work all along. Each step dropped something redundant. The shortest version of the path Kubernetes runs today, kubelet to containerd to runc, is what’s left once you stop translating between Docker’s interface and the runtime underneath, and just talk to containerd.