53

Install cri-dockerd Container Runtime on Kubernetes

Theme: why Docker needs a shim post-1.24 and how to wire one to the kubelet.

Run it in the public monorepo

This course is built around the public HelixWorks Kubernetes Lab monorepo. The excerpt below is runnable source, not pseudocode.

Source: services/Dockerfile

FROM python:3.13-alpine
ARG SERVICE
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 SERVICE_NAME=${SERVICE}
WORKDIR /workspace
COPY services /workspace/services
RUN pip install --no-cache-dir -r /workspace/services/requirements.txt
RUN mkdir -p /data /workspace/.lab && chown -R 65532:65532 /data /workspace/.lab
USER 65532:65532

Code to reality

Declared intent
Build every Python microservice from one DRY runtime recipe while selecting one service at build time.
Interpreter
The Docker builder substitutes SERVICE, installs pinned dependencies, and creates immutable filesystem layers.
Software effect
Each image shares the runtime contract but starts only its single-responsibility service module.
Hardware effect
The resulting non-root container receives its own process, filesystem, CPU, memory, and network isolation.
Observable evidence
Image metadata, non-root process identity, health endpoint, and service-specific response prove the runtime.

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.

Graph legend — each Kubernetes node maps a dock concept to the real runtime stack:

Graph nodeMaps toWhat it does
kubeletthe node kubeletTells the runtime what to run; never runs containers itself
CRI socketthe Unix socket (e.g. cri-dockerd.sock)The standard endpoint the kubelet dials
cri-dockerd shimthe cri-dockerd processAdapts Docker Engine to the CRI
container runtimedockerd/containerd + runcActually 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 nodeMaps toWhat it does
api-server / etcd / scheduler / controller-managercontrol planeUnchanged by the runtime choice on a worker
kubelet dials a CRI socket, cri-dockerd shims Dockerworker node kubelet + cri-dockerdWhere 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 nodeMaps toWhat it does
kubelet -->CRIcontainerdcontainerd CRI pluginNative path: kubelet talks CRI straight to containerd
containerd --> runcruncSpawns the container process
kubelet -->CRIcri-dockerdthe shimDocker path: kubelet talks CRI to cri-dockerd
cri-dockerd --> dockerd --> containerd --> runcDocker stackExtra 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.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.

Graph legend — each node is a real step in starting a pod through cri-dockerd:

Graph nodeMaps toWhat it does
Scheduler assigns pod to nodekube-schedulerBinds the pod to the Docker-Engine node
kubelet dials CRI socketunix:///var/run/cri-dockerd.sockThe endpoint the kubelet connects to
cri-dockerd translates CRI to Docker APIthe shimConverts CRI calls into Docker Engine calls
dockerd pulls image and creates containerdockerdFetches the image and creates the container
dockerd uses containerd and runccontainerd + runcLow-level creation of the process
Process runs in namespaces and cgroupsLinux primitivesIsolates and limits the container
kubelet reports container statusCRI status callsReports 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 nodeMaps toWhat it does
Scheduler assigns shim-test to worker-dockerkube-schedulerPlaces the test pod on the Docker node
kubelet on nodethe node kubeletReceives the pod and drives the runtime
dial CRI socket cri-dockerd.sock--cri-socket endpointConnects the kubelet to cri-dockerd
cri-dockerd shim translates CRI to Docker APIthe shimBridges CRI to Docker Engine
dockerd pulls nginx and creates the containerdockerdPulls nginx:1.27 and creates the container
dockerd delegates to containerd / runccontainerd + runcPerforms low-level container start
process runs in namespaces and cgroupssystemd cgroup driverRuns nginx isolated, cgroups via systemd
kubelet reports Ready statusCRI statusMarks 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/join with --cri-socket
  • [ ] Used crictl --runtime-endpoint to list pods/containers
  • [ ] Stated when to prefer containerd over cri-dockerd