How to Install containerd as a Container Runtime on RHEL 7

containerd is a high-performance, industry-standard container runtime that serves as the foundation beneath both Docker and Kubernetes. When Kubernetes deprecated the Dockershim in version 1.20 and removed it in version 1.24, containerd became the most widely adopted direct container runtime for Kubernetes clusters. Installing containerd directly without Docker as an intermediary reduces overhead, simplifies the software stack, and aligns with how modern managed Kubernetes services like Amazon EKS and Google GKE operate internally. On RHEL 7, where stability and a minimal attack surface are organizational priorities, using containerd as the sole container runtime is the preferred approach for new Kubernetes deployments. This tutorial covers downloading containerd from GitHub releases, configuring it as a systemd service, customizing /etc/containerd/config.toml, using nerdctl as a Docker-compatible CLI, managing containers through containerd namespaces, and integrating the runtime with kubeadm.

Prerequisites

  • RHEL 7.x system with root or sudo access
  • Active Red Hat subscription or EPEL repository configured
  • curl, tar, and systemctl available on the system
  • Internet access from the server to download containerd and CNI plugins
  • SELinux and firewalld enabled (we will configure them appropriately)
  • At least 2 GB disk space available under /var/lib/containerd

Step 1: Install Required Dependencies

containerd requires runc as its low-level container executor and CNI plugins for container networking. Start by installing system libraries:

yum install -y libseccomp libseccomp-devel 
  device-mapper-persistent-data lvm2

Download and install the runc binary from its official GitHub releases. runc is the OCI-compliant container runtime that containerd delegates to for actually starting processes:

curl -fsSL -o /usr/local/sbin/runc 
  https://github.com/opencontainers/runc/releases/download/v1.1.12/runc.amd64

chmod 755 /usr/local/sbin/runc
runc --version

Install CNI plugins, which are required for pod-to-pod networking inside Kubernetes:

mkdir -p /opt/cni/bin

curl -fsSL 
  https://github.com/containernetworking/plugins/releases/download/v1.4.0/cni-plugins-linux-amd64-v1.4.0.tgz 
  | tar -xz -C /opt/cni/bin

ls /opt/cni/bin

Step 2: Download and Install containerd Binaries

Download the containerd binary tarball directly from the official GitHub release page. Using the tarball avoids package manager conflicts with any existing Docker installation:

CONTAINERD_VERSION="1.7.14"

curl -fsSL 
  https://github.com/containerd/containerd/releases/download/v${CONTAINERD_VERSION}/containerd-${CONTAINERD_VERSION}-linux-amd64.tar.gz 
  -o /tmp/containerd.tar.gz

tar -xvzf /tmp/containerd.tar.gz -C /usr/local

# Verify the binaries were installed correctly
ls -la /usr/local/bin/containerd*

The tarball extracts into /usr/local/bin/ and includes containerd, containerd-shim-runc-v1, containerd-shim-runc-v2, and the low-level ctr client binary.

Step 3: Install containerd as a systemd Service

containerd ships with an official systemd unit file. Download and install it so containerd starts automatically on boot:

curl -fsSL 
  https://raw.githubusercontent.com/containerd/containerd/main/containerd.service 
  -o /etc/systemd/system/containerd.service

systemctl daemon-reload
systemctl enable containerd
systemctl start containerd
systemctl status containerd

Verify that the containerd UNIX socket is present, which confirms the daemon is running and accepting connections:

ls -la /run/containerd/containerd.sock

Step 4: Generate and Configure /etc/containerd/config.toml

containerd’s behavior is fully controlled by /etc/containerd/config.toml. Generate the default configuration as a starting point:

mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml

The most critical setting for Kubernetes compatibility is enabling systemd cgroup management. Locate the runc options section and verify the current value:

grep -n "SystemdCgroup" /etc/containerd/config.toml

Set SystemdCgroup = true to enable cgroup v2 compatibility, which is required when using systemd as the init system on RHEL 7:

sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' 
  /etc/containerd/config.toml

# Confirm the change was applied
grep "SystemdCgroup" /etc/containerd/config.toml

Configure the Pause (Sandbox) Image

The pause container image is used by Kubernetes to hold the network namespace for each pod. Ensure it matches the version expected by your Kubernetes release:

grep "sandbox_image" /etc/containerd/config.toml

Update to the correct pause image version if needed:

sed -i 's|sandbox_image = "registry.k8s.io/pause:3.8"|sandbox_image = "registry.k8s.io/pause:3.9"|' 
  /etc/containerd/config.toml

Restart containerd to apply all configuration changes:

systemctl restart containerd
systemctl status containerd

Step 5: Install nerdctl as a Docker-Compatible CLI

The built-in ctr tool is intentionally low-level and lacks many features developers expect. nerdctl provides a Docker-compatible command-line interface for containerd, supporting the same syntax for run, pull, push, build, and compose operations:

NERDCTL_VERSION="1.7.4"

curl -fsSL 
  https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz 
  | tar -xz -C /usr/local/bin

nerdctl version

Test basic container operations with nerdctl:

# Pull an image
nerdctl pull nginx:stable

# Run a container in the background
nerdctl run --rm -d -p 8080:80 --name testnginx nginx:stable

# List running containers
nerdctl ps

# Check the container responds
curl http://localhost:8080/

# Stop and remove it
nerdctl stop testnginx

Step 6: Understand containerd Namespaces

containerd uses an internal namespace model to isolate resources between different consumers. Kubernetes uses the k8s.io namespace, while nerdctl and interactive users work in the default namespace. This separation ensures that docker ps-style commands do not accidentally list or interfere with Kubernetes-managed containers.

# List all containerd namespaces
ctr namespaces list

# List images visible to Kubernetes
ctr -n k8s.io images list

# List running containers in the Kubernetes namespace
ctr -n k8s.io containers list

# Use nerdctl with an explicit namespace
nerdctl --namespace k8s.io ps -a

When debugging a Kubernetes pod issue at the containerd level, always specify -n k8s.io to see the resources Kubernetes has created. The default namespace will be empty for pods started by Kubernetes.

Step 7: Configure Kernel Modules and Sysctl for Kubernetes

Before integrating containerd with kubeadm, load the required kernel modules and configure network settings on RHEL 7:

cat > /etc/modules-load.d/containerd.conf <<EOF
overlay
br_netfilter
EOF

modprobe overlay
modprobe br_netfilter
cat > /etc/sysctl.d/99-kubernetes.conf <<EOF
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

sysctl --system

Confirm both modules are loaded successfully:

lsmod | grep overlay
lsmod | grep br_netfilter

Step 8: Integrate containerd with kubeadm

When running kubeadm init or kubeadm join, specify the containerd CRI socket explicitly to ensure kubeadm does not accidentally detect another runtime:

# Initialize a Kubernetes control plane with containerd
kubeadm init 
  --pod-network-cidr=10.244.0.0/16 
  --cri-socket unix:///run/containerd/containerd.sock

For joining worker nodes to an existing cluster:

kubeadm join 192.168.1.100:6443 
  --token abcdef.0123456789abcdef 
  --discovery-token-ca-cert-hash sha256:<your-hash> 
  --cri-socket unix:///run/containerd/containerd.sock

After the cluster is initialized, verify that Kubernetes correctly identifies containerd as the runtime:

kubectl get nodes -o wide
# The CONTAINER-RUNTIME column should read: containerd://1.7.14

Step 9: Open Required Firewall Ports on RHEL 7

Use firewalld to open all ports required for Kubernetes cluster communication on RHEL 7:

# Control plane node ports
firewall-cmd --permanent --add-port=6443/tcp       # Kubernetes API server
firewall-cmd --permanent --add-port=2379-2380/tcp  # etcd server
firewall-cmd --permanent --add-port=10250/tcp      # kubelet API
firewall-cmd --permanent --add-port=10251/tcp      # kube-scheduler
firewall-cmd --permanent --add-port=10252/tcp      # kube-controller-manager

# Worker node ports
firewall-cmd --permanent --add-port=10250/tcp      # kubelet API
firewall-cmd --permanent --add-port=30000-32767/tcp  # NodePort services

firewall-cmd --reload
firewall-cmd --list-ports

Conclusion

You have installed containerd on RHEL 7 by downloading the binary tarball from official GitHub releases, installing runc and CNI plugins as dependencies, registering containerd as a systemd service that starts automatically on boot, generating and customizing /etc/containerd/config.toml with systemd cgroup support and the correct pause image, installing nerdctl as a user-friendly Docker-compatible CLI, explored containerd’s internal namespace model for debugging Kubernetes workloads, and configured the CRI socket endpoint for kubeadm cluster initialization. containerd’s lean architecture, active CNCF maintenance, and native Kubernetes integration make it the optimal container runtime for RHEL 7 production environments. For further optimization, configure a local container registry mirror in config.toml under the plugins."io.containerd.grpc.v1.cri".registry section to reduce image pull latency, and monitor containerd performance using its built-in Prometheus metrics endpoint at localhost:1338/metrics on each node.