Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

containerd: The Container Runtime Explained

By Kokil Thapa | Last reviewed: September 2026

Kubernetes nodes do not run your application code directly. They delegate execution to a containerd runtime that pulls images, creates sandboxes, and starts processes through OCI-compliant runtimes like runc. If you operate Linux servers or clusters, confusing Docker CLI tooling with the actual execution layer creates blind spots during outages. This guide explains what containerd does, how the Container Runtime Interface (CRI) connects the kubelet to it, and how to manage the containerd service on production nodes.

When I configure server environments for clients moving from legacy monoliths to microservices—as covered in my guide on migrating Laravel monoliths to microservices—the runtime choice affects long-term maintainability. Many engineers still treat the Docker daemon as the execution layer. That confusion leads to bloated nodes, wider attack surfaces, and upgrade paths that break without warning. Stripping away GUI and build tooling, containerd provides a predictable API-driven foundation aligned with how modern Linux server administration actually works.

What is the containerd runtime and why does it matter?

containerd is a daemon process that exposes a gRPC API for managing container lifecycles. It does not build images. It pulls them, unpacks them into snapshots, creates namespaces and cgroups, and hands process execution to a compliant OCI runtime such as runc. Docker Engine embeds containerd internally, but Kubernetes nodes typically run containerd standalone to avoid an extra daemon hop.

The project graduated as a CNCF project and ships as the default runtime on most Kubernetes distributions. Major cloud providers—AWS EKS, Google GKE, Azure AKS—all rely on containerd on worker nodes. Version 2.x introduced a rewritten API surface and improved snapshotter plugins. Check your node with containerd --version during any cluster upgrade planning.

KubeletNode AgentgRPC / CRIcontainerdImage ServiceSnapshotterTask ManagerOCI SpecruncNamespacesCgroups
containerd runtime architecture: orchestration via CRI, management inside containerd, kernel isolation via runc.

This separation decouples orchestration innovation from execution stability. When Kubernetes updates scheduling logic, containerd remains unaffected. When a new storage driver emerges, only the snapshotter component needs updating. For production systems where uptime is non-negotiable, this modularity reduces risk compared to monolithic daemons that bundle networking, building, and execution into one binary.

On a real client project running booking workloads on Kubernetes—similar to platforms like Adventure Third Pole Trek—understanding this stack shortened incident response. Operators who know where the kubelet ends and containerd begins fix ContainerCreating stalls faster than teams searching Docker logs that do not exist on the node.

How does the Container Runtime Interface connect Kubernetes to containerd?

The Container Runtime Interface (CRI) is the gRPC protocol that lets kubelets communicate with any compliant runtime without code changes. Before CRI, integrating a new runtime required forking Kubernetes itself. Now any runtime implementing the CRI service plugs in directly. This standardization is why the containerd runtime became the default for most distributions after dockershim removal in Kubernetes 1.24.

CRI services inside containerd

containerd implements two primary CRI services as plugins. The ImageService handles pulling, listing, and removing container images. The RuntimeService manages pod sandboxes, container creation, start and stop operations, and exec calls. Both run inside the containerd daemon and are configured through /etc/containerd/config.toml.

# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri"]
  sandbox_image = "registry.k8s.io/pause:3.10"
  [plugins."io.containerd.grpc.v1.cri".containerd]
    default_runtime_name = "runc"
    [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

A common mistake during server hardening is leaving the default CRI configuration untouched. On Ubuntu 24.04 LTS servers running systemd, failing to set SystemdCgroup = true causes resource limits to be silently ignored. containerd defaults to the cgroupfs driver while systemd expects unified hierarchy management. Always verify this matches your host init system before deploying workloads. The official Kubernetes container runtime documentation covers the required settings per distribution.

Pod sandbox creation flow

When a pod is scheduled, the kubelet first requests a sandbox via RunPodSandbox. containerd starts a pause container that holds the network namespace open. CNI plugins then attach interfaces. Only after the sandbox reports ready does containerd create application containers within that shared namespace. If networking fails here, pods stay stuck in ContainerCreating indefinitely.

KubeletcontainerdCNI PluginruncRunPodSandbox()Create Pause ContainerSetup Network NSIP AssignedSandbox ReadyCreate + Start()
CRI sequence during pod initialization—critical for debugging containerd runtime startup failures.

How do containerd and Docker differ in production?

This distinction causes more confusion than almost any other topic in modern DevOps. Docker Engine includes containerd as a subcomponent but wraps it with additional layers: the Docker daemon (dockerd), BuildKit, volume management, and the familiar CLI. When you run docker run, the request flows through dockerd → containerd → runc. In Kubernetes, that extra hop adds complexity without benefit since the cluster manages images, networking, and volumes independently.

FeatureDocker Enginecontainerd Standalone
Primary use caseDeveloper workstations, CI buildsProduction Kubernetes nodes, edge devices
Image buildingBuilt-in BuildKitNot included—use Buildah or Kaniko
Memory footprint~150–300 MB idle~30–60 MB idle
Attack surfaceLarger (daemon + builder + CLI)Minimal (daemon + shim)
Kubernetes integrationVia dockershim (removed in v1.24+)Native CRI support
ConfigurationJSON daemon.jsonTOML config.toml
CLI toolsdockerctr (debug), crictl (K8s)

For teams transitioning from Docker-based workflows, the biggest adjustment is losing the docker command on production nodes. Use crictl for Kubernetes-aware debugging or ctr for direct containerd inspection. I recommend installing both on every node. As noted in my article on hiring DevOps engineers in Nepal, proficiency with these lower-level tools separates operators who troubleshoot real incidents from those dependent on dashboard abstractions.

When Docker still makes sense

Docker remains excellent for local development and CI pipelines where image building is required. The ergonomic benefits of docker-compose for multi-service testing outweigh the overhead on developer laptops. See our guides on containerizing a Laravel app and installing Docker on Ubuntu for workstation setup. However, deploying Docker Engine on production Kubernetes nodes solely because "that is what we have always used" introduces unnecessary risk. Organizations that migrated early report fewer node-level incidents and faster security patching cycles.

How do you manage the containerd service on Linux?

The containerd service is a systemd unit on most Linux distributions. It starts at boot, listens on a Unix socket at /run/containerd/containerd.sock, and stores state under /var/lib/containerd/. Understanding service management is essential because a stopped or misconfigured daemon blocks every pod on the node.

systemd commands for the containerd service

# Check containerd service status
sudo systemctl status containerd

# Restart after config.toml changes
sudo systemctl restart containerd

# Enable at boot
sudo systemctl enable containerd

# Follow daemon logs
sudo journalctl -u containerd -f --no-pager

After editing config.toml, always validate syntax before restarting. Run sudo containerd config dump to print the effective configuration including defaults you did not explicitly set. A malformed TOML file prevents the containerd service from starting, which immediately marks the node as NotReady in Kubernetes. For broader systemd patterns, see our guide on managing services on Linux.

Key paths and sockets

  • /etc/containerd/config.toml — main configuration file
  • /run/containerd/containerd.sock — gRPC and CRI socket (mode 0660, root-only)
  • /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/ — filesystem layers
  • /var/log/pods/ — CRI-format container logs written by the kubelet
  • /etc/cni/net.d/ — CNI plugin configuration consumed during sandbox setup

On nodes I maintain through enterprise application deployments, I add a systemd drop-in only when needed—for example, raising file descriptor limits on high-density nodes. Avoid unnecessary overrides. The stock unit file from your distribution vendor is tested against their kernel and cgroup configuration.

systemdstart containerdcontainerddaemon runningCRI Socketcontainerd.sockLoad config.tomlCRI + snapshotter pluginsKubelet connectsPods scheduled to nodeNode Ready
containerd service lifecycle—from systemd boot through CRI socket availability to kubelet pod scheduling.

How do you debug containerd pod failures effectively?

Debugging the containerd runtime requires shifting from interactive shells to API inspection. Since there is no persistent shell session like docker exec provides by default, you query state through CRI-compatible tools. The crictl utility mirrors Docker CLI semantics while speaking CRI natively.

Essential debugging commands

  • crictl ps -a — list all containers including stopped ones with exit codes
  • crictl inspect <container-id> — full JSON metadata including mounts and limits
  • crictl logs <container-id> — stream stdout and stderr from runtime log files
  • ctr -n k8s.io content ls — inspect raw image blobs in the content store
  • journalctl -u containerd -f — follow daemon logs with structured output
# Check if containerd recognizes a specific image
sudo ctr -n k8s.io images ls | grep nginx

# Inspect runtime configuration
sudo containerd config dump | grep -A5 'runtimes.runc'

# Verify CNI plugin health
sudo crictl info | jq '.config.storageRoot'

A pattern I have seen repeatedly involves stale container state after unclean shutdowns. When nodes reboot abruptly, containerd may retain references to containers whose processes no longer exist. Running crictl rm --force $(crictl ps -aq) clears orphaned entries safely. Always check snapshot directories for leaked filesystem layers that consume disk space invisibly. For broader cluster diagnostics, see our Kubernetes troubleshooting field guide and CrashLoopBackOff debugging guide.

Pod Not Runningcrictl ps -a shows it?NOYESCheck kubelet logsImage pull / CNI failcrictl inspect IDExit code + mountsExit Code Check137=OOM, 1=App ErrorCheck containerd svcsystemctl + config.toml
Decision tree for troubleshooting containerd runtime failures—branch on crictl output and containerd service health.

Log format and aggregation

containerd writes container logs to /var/log/pods/<namespace>_<pod-name>_<uid>/<container-name>/ as rotated JSON files. Unlike Docker's json-file driver, containerd uses CRI logging format with timestamp and stream fields. Configure your log collector—Fluent Bit, Vector, or Promtail—to parse this format explicitly. When inspecting raw JSON locally, a JSON formatter helps read nested CRI log entries quickly.

How should you secure the containerd runtime in production?

Security hardening starts with minimizing privileges. containerd supports rootless mode where the daemon runs as an unprivileged user, mapping container UIDs to high-numbered host ranges. This prevents container escapes from gaining root access to the host.

Runtime security controls

  1. Enable AppArmor or SELinux profiles: Default configs may disable mandatory access control. Create custom profiles restricting syscalls beyond basic seccomp defaults.
  2. Restrict image registries: Configure registry mirrors and allowlists in config.toml to prevent pulls from untrusted sources.
  3. Block privileged containers: Enforce Pod Security admission controllers cluster-wide to block privilege escalation vectors.
  4. Audit socket permissions: The CRI socket grants full runtime control. Keep ownership at root:root with mode 0660. Never expose it over TCP.
  5. Scan images before deploy: Integrate Trivy image scanning into your CI pipeline before images reach production nodes.

For legal-tech platforms handling sensitive case data, these controls are foundational requirements. When architecting secure client portals as described in my overview of legal tech solutions for law firms, runtime isolation provides defense-in-depth alongside application-layer encryption. Defense assumes breach; layered containment limits blast radius when vulnerabilities surface. The official containerd documentation covers rootless setup and plugin configuration in detail.

Key Takeaways

  • The containerd runtime executes containers on Kubernetes nodes—it is not a replacement for Docker on your laptop.
  • CRI is the gRPC bridge between kubelet and containerd; misconfigured SystemdCgroup silently breaks resource limits.
  • Manage the containerd service with systemctl; validate config.toml with containerd config dump before restarting.
  • Use crictl on production nodes instead of docker—it speaks CRI natively and shows Kubernetes-aware state.
  • Debug pod failures by checking whether crictl sees the container, then inspect exit codes and containerd service logs.
  • Harden nodes with registry allowlists, Pod Security policies, image scanning, and restricted CRI socket permissions.

People Also Ask

Is containerd the same as Docker?

No. Docker Engine includes containerd internally but adds dockerd, BuildKit, and a developer CLI on top. Kubernetes nodes run containerd standalone and talk to it directly through CRI, skipping dockerd entirely. You build images with Docker or Buildah in CI; you run them with containerd on the node.

What is the containerd service and how do I restart it?

The containerd service is a systemd unit that runs the containerd daemon. Restart it with sudo systemctl restart containerd after editing /etc/containerd/config.toml. Always run containerd config dump first to confirm syntax. A failed restart marks the Kubernetes node as NotReady.

Can I use docker commands on a Kubernetes node running containerd?

Not reliably. Production Kubernetes nodes typically have no Docker daemon installed. Use crictl for pod and container operations, or ctr -n k8s.io for low-level containerd inspection. Install crictl alongside your cluster tooling on every node.

Why did Kubernetes remove dockershim?

dockershim was a maintenance burden that added an unnecessary translation layer between the kubelet and Docker's containerd instance. CRI provides a standard interface any runtime can implement. containerd, CRI-O, and others now plug in directly, simplifying node architecture and reducing attack surface.

Next Steps for Your Infrastructure

Understanding the containerd runtime transforms how you operate production infrastructure. You move from treating containers as black boxes to knowing the precise mechanisms governing lifecycle, security boundaries, and failure modes. That knowledge pays off during incident response, capacity planning, and security audits.

Start by confirming containerd is your node runtime with crictl info. Review your CRI configuration against current best practices for your Kubernetes version. Practice debugging with crictl on a staging node until the commands feel natural. If you are deploying your first cluster, our Kubernetes basics guide and GitLab CI pipeline tutorial cover the full path from image build to node execution.

When you need help hardening production nodes or migrating from Docker-based workflows, contact us to discuss your infrastructure requirements. You can also reach out directly for a technical review of your current container runtime setup.

Frequently Asked Questions

Containerd is a lightweight container runtime that manages the complete lifecycle of containers, including image transfer, storage, execution, and low-level OS interactions. Unlike Docker, which includes a full platform with CLI, build tools, and orchestration features, containerd focuses solely on runtime operations. In my experience deploying production systems on Ubuntu 24, containerd serves as the underlying engine for Kubernetes while Docker remains better suited for local development workflows where developers need integrated build and compose functionality without managing separate components.

Yes, containerd is completely open-source under the Apache 2.0 license with no licensing fees or enterprise tiers. The only costs are infrastructure and operational overhead. For Nepal-based teams budgeting in NPR, expect zero software costs but allocate Rs 15,000-30,000 monthly (~USD 110-220) per server for managed cloud instances or equivalent bare metal, plus engineering time for configuration and maintenance. This makes it significantly cheaper than proprietary container platforms requiring per-node licensing.

Choose containerd when running Kubernetes clusters, needing minimal attack surface, or optimizing resource usage on constrained servers. Docker adds unnecessary layers for pure orchestration workloads. On legal-tech portals I have deployed, containerd reduced memory overhead by 15-20% compared to Docker daemon installations. Stick with Docker if your team needs docker-compose for local development, requires built-in image building in CI pipelines, or lacks dedicated DevOps expertise for managing lower-level runtime configuration and troubleshooting.

Run sudo apt update && sudo apt install -y containerd.io to get the latest stable release from Docker's official repository. After installation, generate default configuration with sudo containerd config default | sudo tee /etc/containerd/config.toml, then enable SystemD cgroup driver by setting SystemdCgroup = true under [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]. Restart with sudo systemctl restart containerd and verify with crictl info. Always pin versions in production rather than using latest tags to ensure reproducible deployments across environments.

Yes, containerd fully implements the Open Container Initiative runtime and image specifications. It pulls and runs any OCI-compliant image from registries like Docker Hub, GHCR, or private Harbor instances without modification. Images built with Docker, Podman, or Buildah work identically. In practice, this means you can migrate existing containerized applications to containerd without rebuilding images or changing Dockerfiles. The runtime handles layer unpacking, snapshot management, and filesystem mounting according to OCI standards regardless of which tool originally created the image artifact.

Containerd exposes the Container Runtime Interface natively through its CRI plugin, eliminating the need for dockershim or intermediate proxies. Kubernetes kubelet communicates directly via Unix socket at /run/containerd/containerd.sock. This direct integration reduces latency and failure points compared to Docker-based setups. When configuring kubeadm clusters, specify --container-runtime-endpoint=unix:///run/containerd/containerd.sock during initialization. On production clusters I manage, this native CRI support simplifies debugging since crictl commands map directly to Kubernetes pod and container states without translation layers.

Most failures stem from misconfigured cgroup drivers, missing kernel modules, or permission issues. Check journalctl -u containerd for specific errors. If seeing "failed to create shim task," verify SystemdCgroup matches your init system and that overlayfs module is loaded via lsmod | grep overlay. Permission denied errors usually indicate incorrect /var/lib/containerd ownership—fix with sudo chown -R root:root /var/lib/containerd. After config changes, always validate syntax before restarting using containerd config dump. Keep systemd unit overrides minimal to avoid masking upstream defaults during package upgrades.

Edit /etc/containerd/config.toml and add registry credentials under [plugins."io.containerd.grpc.v1.cri".registry.configs."registry.example.com".auth] with username and password fields. For token-based auth, use identitytoken instead. Alternatively, configure hosts.toml files in /etc/containerd/certs.d/registry.example.com/ for more granular control over mirrors and TLS verification. Reload configuration with sudo systemctl reload containerd after changes. Test with crictl pull registry.example.com/image:tag to confirm authentication works before deploying pods. Never store plaintext passwords in version-controlled configs; use environment variables or secrets managers in CI pipelines.

Yes, containerd supports rootless mode through user namespace remapping and unprivileged container execution. Configure by running containerd-rootless-setuptool.sh install which creates user-scoped systemd units and adjusts storage paths to ~/.local/share/containerd. Rootless containers cannot bind privileged ports below 1024 or access host devices directly, but provide strong isolation for multi-tenant workloads. This is particularly valuable for shared development servers or legal document processing systems where tenant data must remain isolated. Performance overhead is minimal on modern kernels with cgroup v2 and idmapped mounts enabled.

Containerd automatically removes unused images based on configurable thresholds in config.toml under [plugins."io.containerd.grpc.v1.cri".containerd]. Set discard_unpacked_layers = true to delete extracted layers when base images are removed. Manual cleanup uses ctr content prune or crictl rmi --prune to reclaim disk space. Schedule regular garbage collection via systemd timer for production systems accumulating stale images from frequent deployments. On servers with limited storage, I configure aggressive GC policies retaining only actively referenced images. Monitor /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs usage to prevent disk exhaustion during high-frequency deployment cycles.

Containerd outputs structured logs to journald by default; configure log level and format in config.toml under [debug]. For metrics, enable Prometheus endpoint via [plugins."io.containerd.grpc.v1.cri".metrics] binding to localhost:1338/metrics. Export these to Grafana dashboards tracking container start latency, image pull duration, and runtime errors. Use crictl stats for real-time container resource consumption. Integrate with OpenTelemetry for distributed tracing across container lifecycle events. On production systems, I forward containerd logs to centralized logging alongside application output to correlate runtime failures with business logic errors during incident response.

Upgrade containerd using rolling node maintenance: cordon node with kubectl cordon, drain pods via kubectl drain --ignore-daemonsets --delete-emptydir-data, then upgrade package with apt install --only-upgrade containerd.io. Verify service health with systemctl status containerd and test runtime with crictl ps before uncordoning. Repeat across cluster nodes sequentially. Since containerd maintains backward compatibility within major versions, in-place upgrades rarely break running containers. Always test upgrade path in staging first. On GitLab CI-managed infrastructure, I automate this sequence with Deployer 7 scripts ensuring consistent rollback procedures if post-upgrade validation fails.

Disable unnecessary plugins in config.toml, restrict Unix socket permissions to 660 with dedicated group, and enable SELinux or AppArmor profiles for container confinement. Use seccomp filters to limit syscalls available to containers. Regularly audit CVE databases for containerd vulnerabilities and patch promptly. Avoid running containers as UID 0 inside namespaces. Enable image signature verification using cosign or Notation to prevent supply chain attacks. On legal-tech platforms handling sensitive documents, I enforce read-only root filesystems and drop all Linux capabilities except those explicitly required by application code.

Containerd typically consumes 30-50MB less RAM than Docker daemon since it omits API server, builder, and network management components. Container start times are comparable, but image pulls may be slightly faster due to reduced abstraction layers. CPU overhead during steady-state operation is negligible for both. The real advantage emerges at scale: on nodes running 50+ containers, containerd's smaller footprint leaves more resources for actual workloads. Benchmarks on Ubuntu 24 show identical throughput for web serving workloads. Choose based on operational requirements rather than micro-benchmarks; the performance delta rarely drives architectural decisions in practice.

Migration is straightforward since both use OCI-compliant images and similar networking models. Export running Docker containers with docker export or rebuild from source Dockerfiles. Update Kubernetes manifests to remove Docker-specific annotations and verify volume mount paths match containerd's snapshotter behavior. Test thoroughly in staging since some Docker convenience features like automatic DNS resolution between linked containers require explicit configuration in containerd. Compose files need conversion to Kubernetes resources or alternative orchestrators. On projects I have migrated, the transition took 2-3 days including testing, primarily addressing networking assumptions baked into legacy application configurations.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: