Install cri-dockerd Container Runtime on Kubernetes
Video: Day 53 — CRI, dockershim removal, and cri-dockerd • Theme: why Docker needs a shim post-1.24 and how to wire one to the kubelet.
Key terms
| Term | Meaning |
|---|---|
| CRI | Container Runtime Interface — the gRPC API the kubelet speaks |
| dockershim | The old in-tree adapter from kubelet to Docker (removed 1.24) |
| cri-dockerd | Mirantis' external shim exposing Docker via CRI |
| containerd | A CRI-native runtime (the common default) |
| runc | The low-level OCI runtime that spawns the process |
| CRI socket | The Unix socket the kubelet dials to reach the runtime |
| crictl | CRI-level debug CLI (runtime-agnostic) |
Problem & solution
The kubelet does not run containers itself — it speaks the CRI to a runtime. Docker never implemented the CRI; the kubelet shipped an in-tree adapter called dockershim. Kubernetes removed dockershim in 1.24, so a stock kubelet can no longer talk to the Docker Engine directly.
Solution: If you must keep Docker Engine on nodes, install cri-dockerd, a standalone shim that exposes Docker through the CRI. Otherwise use a CRI-native runtime like containerd and skip the shim entirely.
The analogy
On the dock, the kubelet foreman never lifts a container by hand, he signals a crane engine that does the actual lifting onto ships. The foreman only knows one standard control plug for talking to cranes: a modern crane speaks it natively, but an older Docker-brand crane needs an adapter spliced onto the plug. In Kubernetes the crane engine is the container runtime, the standard plug is the CRI socket, a native crane is containerd, and the adapter that keeps the Docker engine usable is cri-dockerd.
Graph legend — each Kubernetes node maps a dock concept to the real runtime stack:
| Graph node | Maps to | What it does |
|---|---|---|
| kubelet | the node kubelet | Tells the runtime what to run; never runs containers itself |
| CRI socket | the Unix socket (e.g. cri-dockerd.sock) | The standard endpoint the kubelet dials |
| cri-dockerd shim | the cri-dockerd process | Adapts Docker Engine to the CRI |
| container runtime | dockerd/containerd + runc | Actually pulls images and starts containers |
Where this fits in the cluster
The same cluster entities appear in every day's notes; the diagram below shows where this day's topic fits.
Graph legend — this day touches only the node-level runtime wiring:
| Graph node | Maps to | What it does |
|---|---|---|
| api-server / etcd / scheduler / controller-manager | control plane | Unchanged by the runtime choice on a worker |
| kubelet dials a CRI socket, cri-dockerd shims Docker | worker node kubelet + cri-dockerd | Where the CRI socket and shim live and run |
The runtime stack with and without a shim
The kubelet always speaks CRI. The only question is what answers on the socket.
Graph legend — the two paths differ only in whether a shim sits on the CRI socket:
| Graph node | Maps to | What it does | ||
|---|---|---|---|---|
| kubelet --> | CRI | containerd | containerd CRI plugin | Native path: kubelet talks CRI straight to containerd |
| containerd --> runc | runc | Spawns the container process | ||
| kubelet --> | CRI | cri-dockerd | the shim | Docker path: kubelet talks CRI to cri-dockerd |
| cri-dockerd --> dockerd --> containerd --> runc | Docker stack | Extra hops cri-dockerd adds before runc runs the process |
Note Docker itself uses containerd under the hood, so going through cri-dockerd adds an extra hop the containerd path does not have.
Why dockershim was removed
- dockershim was maintenance burden inside the kubelet for one specific, non-CRI runtime.
- Docker's stack already layers
dockerd -> containerd -> runc; the kubelet can talk to containerd directly and drop two layers. - "Docker images still work": images are OCI-standard, so containerd/CRI-O run the exact same images. Only the runtime daemon changed.
Install cri-dockerd on a node
Done on every node that will use Docker Engine. Docker Engine must already be installed and running.
# 1) confirm Docker is present
docker --version
systemctl status docker
# 2) install cri-dockerd (from the Mirantis release; version pinned to the node arch)
VER=0.3.15
curl -fsSLo cri-dockerd.tgz \
https://github.com/Mirantis/cri-dockerd/releases/download/v${VER}/cri-dockerd-${VER}.amd64.tgz
tar -xzf cri-dockerd.tgz
sudo install -m 0755 cri-dockerd/cri-dockerd /usr/local/bin/cri-dockerd
# 3) install the systemd units that ship with the project
sudo curl -fsSLo /etc/systemd/system/cri-docker.service \
https://raw.githubusercontent.com/Mirantis/cri-dockerd/v${VER}/packaging/systemd/cri-docker.service
sudo curl -fsSLo /etc/systemd/system/cri-docker.socket \
https://raw.githubusercontent.com/Mirantis/cri-dockerd/v${VER}/packaging/systemd/cri-docker.socket
# 4) start it
sudo systemctl daemon-reload
sudo systemctl enable --now cri-docker.socket
systemctl status cri-docker.socket
The shim listens on a Unix socket, typically:
ls -l /var/run/cri-dockerd.sock
Point the kubelet / kubeadm at the CRI socket
The kubelet needs to know which socket to dial. With kubeadm you pass the socket explicitly because more than one runtime may be installed.
# new cluster
sudo kubeadm init --cri-socket unix:///var/run/cri-dockerd.sock \
--pod-network-cidr=10.244.0.0/16
# joining a node
sudo kubeadm join <cp-endpoint>:6443 \
--cri-socket unix:///var/run/cri-dockerd.sock \
--token <token> --discovery-token-ca-cert-hash sha256:<hash>
kubeadm records the socket on the Node object so future commands know it:
kubectl get node <node> -o jsonpath='{.metadata.annotations.kubeadm\.alpha\.kubernetes\.io/cri-socket}'
Verify and debug with crictl
crictl talks the CRI directly and is the runtime-agnostic debug tool (Docker's
docker ps will NOT show kubelet pods when the shim is used through CRI).
# tell crictl which socket to use
sudo crictl --runtime-endpoint unix:///var/run/cri-dockerd.sock ps
sudo crictl --runtime-endpoint unix:///var/run/cri-dockerd.sock pods
sudo crictl info | grep -i runtime
# or persist it
echo 'runtime-endpoint: unix:///var/run/cri-dockerd.sock' | sudo tee /etc/crictl.yaml
kubectl get nodes -o wide
kubectl get node <node> -o jsonpath='{.status.nodeInfo.containerRuntimeVersion}'
# shows e.g. docker://... when running through cri-dockerd
containerd vs cri-dockerd: which to pick
- containerd (or CRI-O): CRI-native, fewer layers, the recommended default
for new clusters; configured at
/etc/containerd/config.tomlwithSystemdCgroup = trueto match the kubelet cgroup driver. - cri-dockerd: choose only when you must keep the Docker Engine daemon on
nodes (legacy tooling,
docker buildon the host). It adds a hop and another component to maintain.
cgroup driver MUST match across kubelet and runtime:
kubelet cgroupDriver: systemd == runtime SystemdCgroup: true
mismatch -> kubelet flaps, pods fail to start
End-to-end: kubelet starts a pod via cri-dockerd
The full path from a scheduled pod to a running container through the shim.
Graph legend — each node is a real step in starting a pod through cri-dockerd:
| Graph node | Maps to | What it does |
|---|---|---|
| Scheduler assigns pod to node | kube-scheduler | Binds the pod to the Docker-Engine node |
| kubelet dials CRI socket | unix:///var/run/cri-dockerd.sock | The endpoint the kubelet connects to |
| cri-dockerd translates CRI to Docker API | the shim | Converts CRI calls into Docker Engine calls |
| dockerd pulls image and creates container | dockerd | Fetches the image and creates the container |
| dockerd uses containerd and runc | containerd + runc | Low-level creation of the process |
| Process runs in namespaces and cgroups | Linux primitives | Isolates and limits the container |
| kubelet reports container status | CRI status calls | Reports back to the api-server |
End-to-end example: join a Docker-Engine node with cri-dockerd
A complete walkthrough on a fresh worker: install cri-dockerd, align the cgroup
driver, join the cluster with --cri-socket, and verify the node goes Ready and
schedules a pod through the shim.
Step 1 — confirm Docker is running and install cri-dockerd.
docker --version
# Docker version 27.1.1, build ...
sudo systemctl is-active docker
# active
VER=0.3.15
curl -fsSLo cri-dockerd.tgz \
https://github.com/Mirantis/cri-dockerd/releases/download/v${VER}/cri-dockerd-${VER}.amd64.tgz
tar -xzf cri-dockerd.tgz
sudo install -m 0755 cri-dockerd/cri-dockerd /usr/local/bin/cri-dockerd
cri-dockerd --version
# cri-dockerd 0.3.15 (HEAD)
Step 2 — install and start the systemd units; confirm the socket.
sudo curl -fsSLo /etc/systemd/system/cri-docker.service \
https://raw.githubusercontent.com/Mirantis/cri-dockerd/v${VER}/packaging/systemd/cri-docker.service
sudo curl -fsSLo /etc/systemd/system/cri-docker.socket \
https://raw.githubusercontent.com/Mirantis/cri-dockerd/v${VER}/packaging/systemd/cri-docker.socket
sudo systemctl daemon-reload
sudo systemctl enable --now cri-docker.socket
systemctl is-active cri-docker.socket
# active
ls -l /var/run/cri-dockerd.sock
# srw-rw---- 1 root docker 0 Jun 22 10:00 /var/run/cri-dockerd.sock
Step 3 — align the cgroup driver (must match the kubelet's systemd).
# Docker daemon -> systemd cgroup driver
sudo tee /etc/docker/daemon.json >/dev/null <<'EOF'
{ "exec-opts": ["native.cgroupdriver=systemd"] }
EOF
sudo systemctl restart docker cri-docker
docker info | grep -i cgroup
# Cgroup Driver: systemd
# Cgroup Version: 2
Step 4 — point crictl at the socket and sanity-check the runtime.
echo 'runtime-endpoint: unix:///var/run/cri-dockerd.sock' | sudo tee /etc/crictl.yaml
sudo crictl info | grep -i runtimeName
# "runtimeName": "docker"
sudo crictl version
# RuntimeName: docker
# RuntimeApiVersion: v1
Step 5 — join the cluster naming the cri-dockerd socket.
sudo kubeadm join 192.168.1.100:6443 \
--token abcdef.0123456789abcdef \
--discovery-token-ca-cert-hash sha256:<hash> \
--cri-socket unix:///var/run/cri-dockerd.sock
# [preflight] Running pre-flight checks
# This node has joined the cluster: ...
# kubeadm records the socket on the Node object
kubectl get node worker-docker -o jsonpath='{.metadata.annotations.kubeadm\.alpha\.kubernetes\.io/cri-socket}'
# unix:///var/run/cri-dockerd.sock
Step 6 — verify the node is Ready and runs a pod through the shim.
kubectl get node worker-docker -o wide
# NAME STATUS ROLES VERSION CONTAINER-RUNTIME
# worker-docker Ready <none> v1.30.2 docker://27.1.1
kubectl run shim-test --image=nginx:1.27 \
--overrides='{"spec":{"nodeName":"worker-docker"}}'
kubectl wait --for=condition=Ready pod/shim-test --timeout=60s
# crictl sees the pod sandbox; plain docker ps does NOT show CRI-managed pods
sudo crictl pods | grep shim-test
# <id> Ready shim-test default ...
Graph legend — each node is a real step in scheduling shim-test through the shim:
| Graph node | Maps to | What it does |
|---|---|---|
| Scheduler assigns shim-test to worker-docker | kube-scheduler | Places the test pod on the Docker node |
| kubelet on node | the node kubelet | Receives the pod and drives the runtime |
| dial CRI socket cri-dockerd.sock | --cri-socket endpoint | Connects the kubelet to cri-dockerd |
| cri-dockerd shim translates CRI to Docker API | the shim | Bridges CRI to Docker Engine |
| dockerd pulls nginx and creates the container | dockerd | Pulls nginx:1.27 and creates the container |
| dockerd delegates to containerd / runc | containerd + runc | Performs low-level container start |
| process runs in namespaces and cgroups | systemd cgroup driver | Runs nginx isolated, cgroups via systemd |
| kubelet reports Ready status | CRI status | Marks the pod Ready to the api-server |
Key takeaways
- The kubelet speaks CRI; Docker never did, so dockershim bridged it.
- dockershim was removed in 1.24 — a stock kubelet cannot talk to Docker directly.
- cri-dockerd is the external shim to keep Docker Engine; containerd needs none.
- Tell kubeadm the runtime with
--cri-socket unix:///var/run/cri-dockerd.sock. - Debug with crictl (not
docker ps); keep the cgroup driver consistent.
Checklist
- [ ] Explained CRI and why dockershim existed and was removed
- [ ] Installed cri-dockerd and enabled
cri-docker.socket - [ ] Ran
kubeadm init/joinwith--cri-socket - [ ] Used
crictl --runtime-endpointto list pods/containers - [ ] Stated when to prefer containerd over cri-dockerd