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: August 2026

If you manage production Linux infrastructure or Kubernetes clusters, understanding containerd: The Container Runtime Explained is no longer optional academic knowledge—it is an operational requirement. While Docker popularized containers, modern orchestration relies directly on containerd as the stable, lightweight engine that actually executes workloads. For developers building scalable systems, whether for global SaaS platforms or local Nepali enterprise applications, distinguishing between the CLI tooling and the underlying runtime prevents critical debugging blind spots.

When I configure server environments for clients, particularly those moving from legacy monoliths to microservices as discussed in my guide on migrating Laravel monoliths to microservices, the choice of runtime dictates long-term maintainability. Many engineers still conflate the Docker daemon with the actual container execution layer. This confusion leads to bloated production nodes, security surface areas that are difficult to audit, and upgrade paths that break unexpectedly. By stripping away the GUI and build tools, containerd provides a predictable, API-driven foundation that aligns with how modern infrastructure automation actually works.

What is containerd and why does it matter?

At its core, 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 the necessary namespaces and cgroups, and hands off process execution to a compliant OCI runtime like runc. This separation of concerns is the defining characteristic of modern container architecture.

Kubelet / ClientOrchestrator AgentgRPC / CRIcontainerdImage ServiceSnapshotterTask ManagerOCI SpecruncKernel NamespacesCgroups / Seccomp
Architecture overview of containerd: The Container Runtime Explained, illustrating the strict separation between orchestration, management, and kernel interaction.

This architecture matters because it decouples innovation at the orchestration layer from stability at the execution layer. When Kubernetes updates its 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 significantly compared to monolithic daemons that bundle networking, building, and execution into a single binary.

How does the Container Runtime Interface work?

The Container Runtime Interface (CRI) is the protocol that allows kubelets to communicate with any compliant runtime without code changes. Before CRI, integrating a new runtime required forking Kubernetes itself. Now, any runtime implementing the CRI gRPC service can plug in seamlessly. This standardization is why containerd has become the default for most distributions.

CRI Implementation Details

In practice, containerd implements two primary CRI services: the ImageService and the RuntimeService. The ImageService handles pulling, listing, and removing container images, while the RuntimeService manages pod sandboxes, container creation, start/stop operations, and exec calls. These services run as plugins within the containerd daemon, configured via the config.toml file.

# /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 I encounter 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 because containerd defaults to cgroupfs driver while systemd expects unified hierarchy management. Always verify this matches your host's init system before deploying workloads.

Pod Sandbox Creation Flow

Understanding the sandbox creation sequence helps diagnose startup failures. When a pod is scheduled, the kubelet first requests a sandbox via RunPodSandbox. containerd allocates a pause container that holds the network namespace open, then configures CNI plugins to attach interfaces. Only after the sandbox reports ready does containerd proceed to create application containers within that shared namespace. If networking fails at this stage, you'll see pods stuck in ContainerCreating state indefinitely.

KubeletcontainerdCNI PluginruncRunPodSandbox()Create Pause ContainerSetup Network NamespaceIP AssignedSandbox Ready ResponseCreateContainer() + Start()
CRI sequence diagram demonstrating the precise order of operations during pod initialization, critical for debugging containerd: The Container Runtime Explained startup issues.

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), the BuildKit builder, volume management, and the familiar CLI. When you run docker run, the request flows through dockerd → containerd → runc. In Kubernetes environments, this 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 (BuildKit)Not included (use Buildah/Kaniko)
Memory Footprint~150-300MB idle~30-60MB idle
Attack SurfaceLarger (daemon + builder + CLI)Minimal (single daemon + shim)
Kubernetes IntegrationVia dockershim (removed in v1.24+)Native CRI support
Configuration FormatJSON daemon.jsonTOML config.toml
CLI Tooldockerctr (debug), crictl (K8s)

For teams transitioning from Docker-based workflows, the biggest adjustment is losing the docker command on production nodes. Instead, use crictl for Kubernetes-aware debugging or ctr for direct containerd inspection. I recommend installing both tools on every node—they're invaluable when pods fail to start and logs show nothing useful. As noted in my article on hiring DevOps engineers in Nepal, proficiency with these lower-level tools separates operators who can 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. However, deploying Docker Engine on production Kubernetes nodes solely because "that's what we've always used" introduces unnecessary risk. The removed dockershim in Kubernetes 1.24 forced many organizations to confront this reality; those who migrated early report fewer node-level incidents and faster security patching cycles.

How do you debug containerd issues effectively?

Debugging containerd requires shifting mental models from interactive shells to API inspection. Since there's no persistent shell session like docker exec provides by default, you must query state through CRI-compatible tools. The crictl utility mirrors Docker CLI semantics while speaking CRI natively, making it the standard diagnostic interface.

Essential Debugging Commands

  • crictl ps -a: List all containers including stopped ones, showing exit codes and timestamps crucial for crash loop analysis
  • crictl inspect <container-id>: Retrieve full JSON metadata including mount points, environment variables, and resource limits applied at runtime
  • crictl logs <container-id>: Stream stdout/stderr directly from the runtime log files, bypassing kubelet aggregation delays
  • ctr -n k8s.io content ls: Inspect raw image blobs stored in containerd's content store, useful for verifying pull integrity
  • journalctl -u containerd -f: Follow daemon logs with structured output; filter by container ID using grep patterns
# Check if containerd recognizes a specific image
sudo ctr -n k8s.io images ls | grep nginx

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

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

A pattern I've 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 pair this with checking /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots for leaked filesystem layers that consume disk space invisibly.

Pod Not Runningcrictl ps -a shows container?NOYESCheck kubelet logsImage pull / CNI failurecrictl inspect <id>Exit code + mountsExit Code Analysis137=OOM, 1=App ErrorVerify Runtime ConfigSystemdCgroup / Snapshots
Practical decision tree for troubleshooting containerd: The Container Runtime Explained failures using crictl and system logs.

Log Aggregation Considerations

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, Promtail) to parse this format explicitly. Misconfigured parsers cause missing logs in observability stacks—a problem I've diagnosed multiple times on client clusters where alerts fired but dashboards showed blank panels.

How should you secure containerd 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. Enable it via systemd user units rather than modifying the main service file.

Runtime Security Controls

  1. Enable SELinux/AppArmor profiles: Default containerd configs disable mandatory access control. Create custom profiles restricting syscalls beyond basic seccomp defaults.
  2. Restrict image registries: Configure [plugins."io.containerd.grpc.v1.cri".registry.mirrors] to allowlist approved sources, preventing accidental pulls from public repositories containing malicious payloads.
  3. Disable privileged containers: Set disable_proc_mount = true and enforce PodSecurity admission controllers cluster-wide to block privilege escalation vectors.
  4. Audit socket permissions: The CRI socket at /run/containerd/containerd.sock grants full runtime control. Restrict ownership to root:root with mode 0660; never expose over TCP.
  5. Rotate credentials regularly: Registry auth tokens stored in /etc/containerd/certs.d/ should follow secret rotation policies aligned with your compliance requirements.

For legal-tech platforms handling sensitive case data, these controls aren't optional extras—they're 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 and access controls. Defense assumes breach; layered containment limits blast radius when vulnerabilities inevitably surface.

Conclusion

Mastering containerd: The Container Runtime Explained transforms how you operate production infrastructure. You move from treating containers as black boxes to understanding the precise mechanisms governing their lifecycle, security boundaries, and failure modes. This knowledge pays dividends during incident response, capacity planning, and security audits—moments when abstraction leaks cost real money and trust.

Start by replacing Docker Engine with standalone containerd on a non-production node. Practice debugging with crictl until the commands feel natural. Review your CRI configuration against current best practices for your Kubernetes version. When you're ready to implement these patterns in your own environment or need expert guidance on container runtime optimization, reach out to discuss your infrastructure needs.

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

Quick Contact Options
Choose how you want to connect me: