Skip to main content
RunBook Academy

KubernetesLXXV · Building a Production ClusterBuilding a production cluster

Container runtime — containerd, runc, and runtime choice

Advanced⏱ ~17 mincontainerdruncnerdctl

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

Not yet marked complete on this device.

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).
Read-only / Safe
$ 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

FailureSymptom
Runtime not runningPods cannot start; kubelet errors
Pause image missingPods cannot be scheduled
CNI plugin missingPods cannot get network
Cgroup mismatchkubelet 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

RuntimeUse
containerdKubernetes production default
cri-oOpenShift / lightweight alternative
gVisorSandboxed user-space kernel
Kata ContainersVM-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

  1. Q1. Which container runtime is the de facto production default for Kubernetes 1.34?

  2. Q2. containerd's `SystemdCgroup = true` configures the cgroup driver to be systemd.

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

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