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.
Where this fits in the cluster
The same cluster entities appear in every day's notes; the <== marks what this day touches.
The runtime stack with and without a shim
The kubelet always speaks CRI. The only question is what answers on the socket.
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.
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 ...
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