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
| 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