Choosing Between containerd and CRI‑O for Production Kubernetes: A Detailed Comparison
This article provides a comprehensive analysis of containerd and CRI‑O as Kubernetes container runtimes, covering their architectures, feature sets, installation procedures, migration strategies, performance benchmarks, best‑practice configurations, troubleshooting tips, and monitoring approaches to help operators decide which runtime best fits a production environment.
Overview
Kubernetes 1.24 removed the dockershim, requiring clusters to switch to a CRI‑compatible runtime. Both containerd and CRI‑O are CNCF‑graduated projects that implement the high‑level runtime layer while delegating container creation to an OCI runtime such as runc.
Technical characteristics
containerd : full‑featured runtime extracted from Docker, supports image management, snapshotting, plugins, and works on both Linux and Windows.
CRI‑O : lightweight runtime designed solely for Kubernetes, implements only the CRI, resulting in a smaller code base and attack surface.
Both use overlayfs by default, rely on runc (or alternatives like crun), and require the pause sandbox image.
Installation and configuration
Common prerequisites
# Check OS and kernel
cat /etc/os-release
uname -r
# Disable swap (Kubernetes requirement)
swapoff -a
sed -i '/swap/d' /etc/fstab
# Load required kernel modules
modprobe overlay
modprobe br_netfilter
# Enable required sysctl settings
cat <<EOF | tee /etc/sysctl.d/99-kubernetes-cri.conf
net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-ip6iptables=1
net.ipv4.ip_forward=1
EOF
sysctl --systemcontainerd installation (CentOS/RHEL)
# Add Docker repo (containerd is shipped there)
yum install -y yum-utils
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# Install containerd
yum install -y containerd.io
# Generate default config and enable SystemdCgroup
containerd config default | tee /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
systemctl enable --now containerdCRI‑O installation (CentOS 8 / RHEL 8)
# Set version variables (CRI‑O version must match K8s version)
OS="CentOS_8_Stream"
VERSION="1.28"
# Add CRI‑O repos
curl -L -o /etc/yum.repos.d/devel:kubic:libcontainers:stable.repo \
https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/${OS}/devel:kubic:libcontainers:stable.repo
curl -L -o /etc/yum.repos.d/devel:kubic:libcontainers:stable:cri-o:${VERSION}.repo \
https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/${VERSION}/${OS}/devel:kubic:libcontainers:stable:cri-o:${VERSION}.repo
# Install CRI‑O
yum install -y cri-o cri-tools
systemctl enable --now crioMigration example: Docker → containerd
The script below performs a node‑by‑node migration for a 50‑node cluster upgrading from Docker to containerd without downtime.
#!/bin/bash
set -euo pipefail
NODE=$(hostname)
# Drain the node
kubectl cordon $NODE
kubectl drain $NODE --ignore-daemonsets --delete-emptydir-data --force --timeout=300s
# Stop Docker and kubelet
systemctl stop kubelet docker containerd
# Install containerd if not present
yum install -y containerd.io
# Generate and adjust config
mkdir -p /etc/containerd
containerd config default | tee /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sed -i 's|sandbox_image = "registry.k8s.io/pause:3.8"|sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.9"|' /etc/containerd/config.toml
# Configure kubelet to use containerd
cat <<EOF | tee /etc/default/kubelet
KUBELET_EXTRA_ARGS=--container-runtime-endpoint=unix:///run/containerd/containerd.sock
EOF
systemctl daemon-reload
systemctl enable --now containerd kubelet
# Wait for node to become Ready
for i in $(seq 1 60); do
STATUS=$(kubectl get node $NODE -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null)
if [ "$STATUS" = "True" ]; then echo "Node ready"; break; fi
echo "Waiting for node ($i/60)"; sleep 5
done
# Uncordon the node
kubectl uncordon $NODE
kubectl get node $NODE -o widePerformance benchmark
A Bash benchmark on identical hardware (8 CPU, 16 GB RAM, SSD) compared container creation time, image pull latency, and memory usage for 50 Pods.
Test Item containerd 1.7.11 CRI‑O 1.28.3 Observation
--------------------------------------------------------------------------
Pod start time (average) 1.8 s 1.6 s CRI‑O slightly faster
nginx image pull (first) 3.2 s 3.4 s Network bound, similar
Runtime process RSS (idle) 45 MB 28 MB CRI‑O uses ~40% less memory
Runtime RSS (50 Pods) 120 MB 65 MB CRI‑O advantage grows with load
conmon RSS (50 Pods) N/A 35 MB CRI‑O creates a conmon per container
Batch create 100 Pods 42 s 38 s CRI‑O marginally fasterBest practices and security
Increase max_concurrent_downloads in /etc/containerd/config.toml to 10 for faster image pulls.
Use overlayfs snapshotter; avoid btrfs/zfs unless required.
Tune containerd GC parameters ( pause_threshold, deletion_threshold) for large clusters.
For CRI‑O, keep conmon_cgroup = "pod" to limit per‑container conmon processes.
Enable the default seccomp profile (both runtimes support it) and restrict default Linux capabilities in CRI‑O.
Configure image signing policies in /etc/containers/policy.json for CRI‑O; use admission webhooks for containerd if needed.
Ensure socket files ( /run/containerd/containerd.sock and /var/run/crio/crio.sock) are owned by root:root with mode 660 and only kubelet has access.
Troubleshooting
Node NotReady – check runtime service status: systemctl status containerd or crio.
Pod stuck in ContainerCreating – verify pause image, CNI plugin path, and that the runtime socket is reachable.
Memory growth – clean exited containers ( crictl rmi --prune) and adjust GC scheduler.
cgroup driver mismatch – both runtime and kubelet must use the systemd driver.
Monitoring
Key metrics can be exposed via the built‑in Prometheus endpoints ( 1338 for containerd, 9537 for CRI‑O): runtime process RSS, container creation latency, error rates, image pull latency, and disk usage.
# Runtime process metrics
ps aux | grep -E "containerd|crio" | grep -v grep
# Container stats
crictl stats
# Prometheus scrape example (prometheus.yml)
scrape_configs:
- job_name: 'containerd'
static_configs:
- targets: ['NODE_IP:1338']
metrics_path: '/v1/metrics'
- job_name: 'crio'
static_configs:
- targets: ['NODE_IP:9537']
metrics_path: '/metrics'Backup and restore
A backup script captures configuration files, CNI settings, crictl config, and the current image list. Restoration involves reinstalling the runtime, restoring the saved files, and restarting services.
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/backup/runtime/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"
# Detect runtime
if systemctl is-active --quiet containerd; then
RUNTIME=containerd
elif systemctl is-active --quiet crio; then
RUNTIME=crio
else
echo "No runtime detected"; exit 1
fi
# Save configs
if [ "$RUNTIME" = "containerd" ]; then
cp /etc/containerd/config.toml "$BACKUP_DIR/"
cp -r /etc/containerd/certs.d "$BACKUP_DIR/" 2>/dev/null || true
else
cp /etc/crio/crio.conf "$BACKUP_DIR/"
cp /etc/containers/registries.conf "$BACKUP_DIR/"
cp /etc/containers/policy.json "$BACKUP_DIR/" 2>/dev/null || true
fi
cp /etc/crictl.yaml "$BACKUP_DIR/" 2>/dev/null || true
cp -r /etc/cni/net.d "$BACKUP_DIR/cni-conf" 2>/dev/null || true
crictl images -o json > "$BACKUP_DIR/images-list.json"Conclusion
Both runtimes satisfy production requirements. containerd offers broader ecosystem support and Windows compatibility, while CRI‑O provides a smaller memory footprint and tighter integration with Red Hat/OpenShift. The choice should align with the existing tech stack, resource constraints, and long‑term operational preferences.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Raymond Ops
Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
