53

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

TermMeaning
CRIContainer Runtime Interface — the gRPC API the kubelet speaks
dockershimThe old in-tree adapter from kubelet to Docker (removed 1.24)
cri-dockerdMirantis' external shim exposing Docker via CRI
containerdA CRI-native runtime (the common default)
runcThe low-level OCI runtime that spawns the process
CRI socketThe Unix socket the kubelet dials to reach the runtime
crictlCRI-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.toml with SystemdCgroup = true to match the kubelet cgroup driver.
  • cri-dockerd: choose only when you must keep the Docker Engine daemon on nodes (legacy tooling, docker build on 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/join with --cri-socket
  • [ ] Used crictl --runtime-endpoint to list pods/containers
  • [ ] Stated when to prefer containerd over cri-dockerd