KubernetesLXXV · Building a Production ClusterBuilding a production cluster
Container runtime — containerd, runc, and runtime choice
What you'll learn
- Choose a container runtime for production
- Configure containerd for Kubernetes
- Verify the runtime end-to-end
- Apply the production discipline of runtime operations
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
The container runtime is the host component that actually runs containers. Kubernetes delegates container lifecycle to the runtime via the Container Runtime Interface (CRI). containerd is the de facto production runtime; this lesson walks its installation, configuration, and the discipline of operating it.
The runtime stack
flowchart TB
K[kubelet] -->|CRI gRPC| CD[containerd]
CD -->|OCI| R[runc]
R -->|syscalls| K2[Kernel]
CD -->|image pull| REG[ImageRegistry]
CD -->|storage| FS["/var/lib/containerd"]
- kubelet calls the runtime through CRI (gRPC).
- containerd manages images, containers, snapshots.
- runc does the low-level OCI runtime work (binary inside the container that exec’s the user process).
- Image registry is the source of container images.
Why containerd
In Kubernetes 1.24+, dockershim was removed. The CRI is the only supported path. Production runtimes:
- containerd: the de facto standard; CNCF project; used by kubeadm default.
- cri-o: lightweight; used by OpenShift; supports pure OCI integration.
- CRI-O / Containerd equivalents: both serve the same purpose.
containerd is the most common because of its API, image management, and integration with kubeadm.
The containerd install
# CentOS / RHEL
sudo dnf install -y containerd.io
sudo systemctl enable --now containerd
# Verify
sudo ctr version
Client:
Version: 1.7.x
Server:
Version: 1.7.x
The containerd configuration
The runtime’s main config file is /etc/containerd/config.toml:
version = 2
[plugins."io.containerd.snapshotter.v1.native"]
# Use the native snapshotter
[plugins."io.containerd.grpc.v1.cri"]
sandbox_image = "registry.k8s.io/pause:3.10"
max_container_log_line_size = 16384
[plugins."io.containerd.grpc.v1.cri.containerd"]
snapshotter = "native"
disable_apparmor = false
disable_cgroup = false
disable_hugetlb_controller = true
[plugins."io.containerd.grpc.v1.cri.cni"]
bin_dir = "/opt/cni/bin"
conf_dir = "/etc/cni/net.d"
[plugins."io.containerd.grpc.v1.cri.containerd.runtimes.runc"]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri.containerd.runtimes.runc.options]
SystemdCgroup = true
The config sets:
- The sandbox (pause) image.
- The CNI integration (where to find CNI plugins + configs).
- The runtime backend (runc v2).
- The cgroup driver (systemd for production).
$ sudo cat /etc/containerd/config.toml | head -20...The pause image
The pause image is the sandbox image that the runtime
sets up as the parent of every container in a Pod. For
Kubernetes 1.34:
sandbox_image = "registry.k8s.io/pause:3.10"
A worker node that cannot pull this image fails to create Pods.
The registry mirrors
For airgapped clusters, containerd’s registry.mirrors configuration:
[plugins."io.containerd.grpc.v1.cri.registry"]
[plugins."io.containerd.grpc.v1.cri.registry.mirrors."docker.io"]
endpoint = ["https://registry.internal.example"]
[plugins."io.containerd.grpc.v1.cri.registry.configs."registry.internal.example".tls]
insecure_skip_verify = false
Mirrors redirect image pulls to internal registries.
The image pull policy
Containerd implements Kubernetes’ image pull policies:
Always: pull every time the Pod is scheduled.IfNotPresent: pull only if not present.Never: never pull; image must be on the host.
containerd handles caching; IfNotPresent is the
default.
The storage and garbage collection
The runtime’s local image cache is at
/var/lib/containerd/. The kubelet’s GCGarbageCollection:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
imageGCHighThresholdPercent: 85
imageGCLowThresholdPercent: 80
When disk usage is above 85%, the GC kicks in to remove unused images until usage falls below 80%.
The runtime failures
| Failure | Symptom |
|---|---|
| Runtime not running | Pods cannot start; kubelet errors |
| Pause image missing | Pods cannot be scheduled |
| CNI plugin missing | Pods cannot get network |
| Cgroup mismatch | kubelet cannot reconcile Pod resource usage |
# Debug
sudo crictl ps -a | head
# CONTAINER ID from the crictl ps output above:
CONTAINER_ID=a3f1c9e2b7d84
sudo crictl logs "$CONTAINER_ID"
sudo ctr -n k8s.io image ls
crictl is the runtime’s CLI for Kubernetes; it works
with containerd directly.
The runtime alternatives
| Runtime | Use |
|---|---|
| containerd | Kubernetes production default |
| cri-o | OpenShift / lightweight alternative |
| gVisor | Sandboxed user-space kernel |
| Kata Containers | VM-based isolation |
For most production clusters, containerd is fine. For multi-tenant with strict isolation, gVisor or Kata may apply.
The runtime update
For a runtime upgrade:
sudo dnf upgrade containerd.io
sudo systemctl restart containerd
sudo systemctl restart kubelet
The kubelet reconnects to the new containerd; Pods continue running (the runtime can hot-restart).
The runtime and cgroup v2
Modern Kubernetes clusters run on cgroup v2:
# Verify the kernel / runtime uses cgroup v2
cat /proc/filesystems | grep cgroup
# nodev cgroup
# nodev cgroup2
containerd with SystemdCgroup = true integrates with
cgroup v2 via systemd.
The runc under containerd
runc is the OCI reference runtime containerd uses for process isolation. It is bundled with containerd:
# Check the binary
ls -la /usr/bin/runc
runc --version
runc supports cgroup v2, namespaces, capabilities, seccomp, AppArmor.
The summary checklist
RUNTIME INSTALLATION CHECKLIST
=============================
[ ] containerd installed
[ ] Configuration with sandbox_image = pause:3.10
[ ] SystemdCgroup = true (matches kubelet)
[ ] CNI bin_dir and conf_dir set
[ ] Registry mirrors configured (if airgapped)
[ ] Pause image pulled on every node
[ ] Image GC thresholds set
[ ] crictl installed for debugging
[ ] Documented in runbook
The discipline
- Validate the runtime before kubelet install. A bad runtime config breaks kubelet.
- Test runtime upgrades on staging. A runtime that changes cgroup drivers breaks the cluster.
- Monitor runtime health. ctr / crictl commands surface issues.
- Document the runtime choice. Containerd is the default; deviations from it should be deliberate.
- Keep registry connectivity. Image pulls are the most common pod-start failure.
Quiz
Knowledge check · 4 questions
Q1. Which container runtime is the de facto production default for Kubernetes 1.34?
Q2. containerd's `SystemdCgroup = true` configures the cgroup driver to be systemd.
Q3. A worker is added to the cluster; pods cannot start because `sandbox_image` is not pulled. Walk the diagnosis.
New worker has containerd installed but pause image is not pulled. kubelet on the worker is registered and Ready, but any Pod scheduled to it stays in ContainerCreating.
Q4. Why is Docker no longer a supported runtime for Kubernetes 1.24+?
Passing score: 75%. Answers are checked in this browser.
Production discipline
- containerd is the default. Deviate only with reason.
- Validate the runtime config. SystemdCgroup = pause image is the minimum.
- Test runtime upgrades. A regression here breaks kubelet.
- Keep the registry accessible. Image pulls fail before Pod starts.
- Document the runtime choice. A deliberate deviation must be recorded.
The runtime is the host component that runs containers. Operating it well is keeping the cluster’s workload runner correct.