← Back to posts
comparison guide self-hosted · · 13 min read

containerd vs CRI-O vs Podman: Best Self-Hosted Container Runtimes 2026

Compare containerd, CRI-O, and Podman — three leading OCI-compliant container runtimes. Learn which one fits your self-hosted infrastructure, from Kubernetes clusters to rootless development environments.

OS
Editorial Team

Every container you run — whether it’s a web server, database, or microservice — depends on a container runtime underneath. The runtime is the low-level software that actually creates, manages, and tears down containers on your host system.

For most of docker’s history, the runtime was bundled together with the CLI, networking, and image management into one monolithic tool. But the ecosystem has since split into specialized, modular components. Today, three OCI-compliant runtimes dominate the self-hosted landscape: containerd, CRI-O, and Podman.

Choosing the right onkubernetes It affects your Kubernetes compatibility, security posture, operational complexity, and whether you can run containers without root privileges.

For related reading, see our Kubernetes distribution comparison for runtime integration details, our container build tools guide for image creation workflows, and our container registry comparison for image storage options.

Why Run Your Own Container Runtime?

Cloud providers abstract the container runtime away from you. But when you self-host, you own the full stack — and the runtime choice has real consequences:

  • Security: Rootless container execution eliminates entire classes of privilege escalation attacks
  • Resource efficiency: Lightweight runtimes use less RAM and CPU on the host, leaving more for your workloads
  • Compliance: Some regulated environments require you to control every layer of the infrastructure stack
  • Vendor independence: Avoiding Docker Desktop licensing fees and vendor lock-in
  • Kubernetes compatibility: Different runtimes integrate differently with the kubelet via the Container Runtime Interface (CRI)

Understanding these trade-offs starts with knowing what each runtime is designed to do.

What Is containerd?

containerd is the industry-standard container runtime that powers Docker, Kubernetes, and countless other projects. Originally extracted from Docker’s internals, it became a standalone CNCF graduated project and is now maintained by the Cloud Native Computing Foundation.

GitHub stats (April 2026):

  • ⭐ 20,594 stars
  • 🍴 3,875 forks
  • 📅 Last active: April 17, 2026
  • 📄 License: Apache-2.0

containerd sits between high-level orchestrators and low-level runtimes like runc. It handles image management, container lifecycle, storage, and networking — but deliberately avoids the CLI and Compose layers. It’s designed as a daemon that other tools talk to via gRPC.

Key Features

  • CNCF Graduated status with enterprise-grade stability
  • CRI plugin built in — provides the Kubernetes Container Runtime Interface out of the box
  • Namespace isolation — multiple tenants can share one containerd daemon safely
  • Snapshotter plugins — supports overlayfs, btrfs, zfs, and native snapshotters
  • NRI (Node Resource Interface) — allows third-party plugins to hook into container lifecycle events
  • Content-addressable storage — images are stored and deduplicated efficiently

Installing containerd

On Ubuntu/Debian:

1
2
3
sudo apt-get update
sudo apt-get install -y containerd
sudo systemctl enable --now containerd

Generate the default configuration:

1
2
3
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
sudo systemctl restart containerd

Verify installation:

1
2
3
4
ctr version
# Client:
#   Version:  2.1.4
#   Runtime:  io.containerd.runc.v2

Using containerd with Kubernetes

containerd is the default runtime for most Kubernetes distributions. The CRI plugin exposes the necessary endpoints automatically:

1
2
3
4
5
6
7
8
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri"]
  sandbox_image = "registry.k8s.io/pause:3.9"

  [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

For interacting with containerd directly (without Docker), use the ctr CLI:

1
2
3
4
5
6
7
8
# Pull an image
sudo ctr images pull docker.io/library/nginx:latest

# Run a container
sudo ctr run -d --net-host docker.io/library/nginx:latest my-nginx

# List containers
sudo ctr containers list

Note that ctr is a debug/admin tool, not a production workflow tool. For day-to-day operations, most users interact with containerd through Docker, Kubernetes, or higher-level tooling.

What Is CRI-O?

CRI-O is a lightweight container runtime built specifically for Kubernetes. Developed by Red Hat and part of the Kubernetes incubator projects, it implements only the Kubernetes Container Runtime Interface — nothing more, nothing less.

GitHub stats (April 2026):

  • ⭐ 5,601 stars
  • 🍴 1,161 forks
  • 📅 Last active: April 17, 2026
  • 📄 License: Apache-2.0

Where containerd tries to be a general-purpose runtime, CRI-O is laser-focused on being the best possible runtime for Kubernetes. It strips away everything that isn’t needed for CRI compliance, resulting in a smaller attack surface and simpler operational model.

Key Features

  • Kubernetes-native — purpose-built for the CRI specification
  • Minimal attack surface — no Docker API compatibility, no general-purpose container management
  • Multiple OCI runtimes — supports runc, crun, and Kata Containers via configuration
  • Image pulling from any registry — supports Docker, OCI, and signed images with sigstore
  • Integrated with OpenShift — the default runtime for Red Hat’s Kubernetes distribution
  • CRI stats API — exposes container and pod metrics for monitoring

Installing CRI-O

On Ubuntu/Debian:

1
2
3
4
5
6
7
8
9
# Set up the repository
OS=xUbuntu_24.04
VERSION=1.31

echo "deb https://pkgs.k8s.io/addons:/cri-o:/stable:/v${VERSION}/deb/ /" | \
  sudo tee /etc/apt/sources.list.d/cri-o.list
sudo apt-get update
sudo apt-get install -y cri-o
sudo systemctl enable --now crio

Verify installation:

1
2
crictl info
# Output shows CRI-O version, runtime configuration, and network settings

Configuring CRI-O

The main configuration file is /etc/crio/crio.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[crio]
log_level = "info"

[crio.runtime]
default_runtime = "crun"
conmon_cgroup = "pod"

[crio.runtime.runtimes.crun]
runtime_path = "/usr/bin/crun"
runtime_type = "oci"

[crio.image]
default_transport = "docker://"
pause_image = "registry.k8s.io/pause:3.9"
pause_command = "/pause"

[crio.network]
network_dir = "/etc/cni/net.d/"
plugin_dirs = ["/opt/cni/bin/", "/usr/libexec/cni/"]

Using CRI-O with crun (a faster C alternative to runc):

1
2
3
4
5
sudo apt-get install -y crun

# Update CRI-O config to use crun
sudo sed -i 's/default_runtime = "runc"/default_runtime = "crun"/' /etc/crio/crio.conf
sudo systemctl restart crio

What Is Podman?

Podman (POD MANager) is a daemonless, rootless container engine developed by Red Hat as part of the containers project. Unlike containerd and CRI-O, Podman provides both the runtime AND the CLI in a single tool — designed as a drop-in replacement for the Docker CLI.

GitHub stats (April 2026):

  • ⭐ 31,432 stars
  • 🍴 3,062 forks
  • 📅 Last active: April 17, 2026
  • 📄 License: Apache-2.0

Podman’s most distinctive feature is that it runs without a daemon. Every podman run command spawns a direct process tree — no background service to manage, no single point of failure. It also supports rootless containers out of the box using user namespaces.

Key Features

  • Daemonless architecture — no persistent daemon; containers are direct child processes
  • Rootless by default — runs containers as non-root users via user namespaces (userns)
  • Docker CLI compatiblealias docker=podman works for most workflows
  • Pod management — groups containers into pods (shared network namespace), similar to Kubernetes pods
  • Quadlet integration — systemd-native container management via .container and .pod files
  • Podman Compose — Docker Compose file compatibility via a separate tool
  • Buildah integration — shares the image build tool from the same project family

Installing Podman

On Ubuntu/Debian:

1
2
sudo apt-get update
sudo apt-get install -y podman

On Fedora/RHEL (usually pre-installed):

1
sudo dnf install -y podman

Verify installation:

1
2
podman --version
# podman version 5.4.0

Running Containers with Podman

Basic usage mirrors Docker exactly:

1
2
3
4
5
6
7
8
# Pull and run a web server
podman run -d --name web -p 8080:80 docker.io/library/nginx:latest

# Run rootless (no sudo needed for most images)
podman run -d --name app -p 3000:3000 docker.io/library/node:20-alpine

# List running containers
podman ps

Using Quadlet for Systemd Integration

Quadlet lets you manage containers as native systemd units — the most robust way to run production containers on a self-hosted server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# /etc/containers/systemd/web.container
[Container]
Image=docker.io/library/nginx:latest
ContainerName=web
PublishPort=8080:80
Volume=/srv/web/html:/usr/share/nginx/html:ro
RestartPolicy=always

[Install]
WantedBy=default.target

Enable and start:

1
2
systemctl daemon-reload
systemctl enable --now web.container

This replaces Docker Compose restart policies and supervisor processes with systemd’s battle-tested service management.

Comparison: containerd vs CRI-O vs Podman

FeaturecontainerdCRI-OPodman
Primary use caseGeneral-purpose runtime, Kubernetes defaultKubernetes-only runtimeDeveloper workstation, daemonless servers
Daemon requiredYes (containerd daemon)Yes (crio daemon)No (daemonless)
Rootless supportNoPartial (via crun)Yes (native, default)
Kubernetes CRIYes (built-in CRI plugin)Yes (purpose-built for CRI)Via Podman kube play / kubelet integration
Docker CLI compatibilityNo (via Docker daemon)NoYes (alias docker=podman)
Compose supportVia Docker ComposeNoVia podman-compose
Pod managementNoVia Kubernetes podsYes (native pod concept)
Systemd integrationVia systemd serviceVia systemd serviceVia Quadlet (.container files)
Stars on GitHub20,5945,60131,432
OCI complianceFullFullFull
Low-level runtimerunc, crun, othersrunc, crun, Katarunc, crun
Image buildNo (external)No (external)Yes (via Buildah integration)
Best forKubernetes clusters, Docker backendMinimal Kubernetes nodesDevelopment, rootless production, systemd-native workflows

When to Use Each Runtime

Choose containerd if:

  • You’re running a Kubernetes cluster and want the battle-tested default
  • You need multi-tenant isolation via containerd namespaces
  • You want broad ecosystem compatibility (Docker uses it internally)
  • You need NRI plugins for custom resource management hooks
  • You’re building a platform on top of containers and need a stable gRPC API

containerd is the safe, proven choice. It’s what Kubernetes used by default after Docker was deprecated as a runtime in v1.24. Most managed Kubernetes services run containerd underneath.

Choose CRI-O if:

  • Your only workload is Kubernetes — no other container use cases
  • You want the smallest possible runtime footprint on each node
  • You’re running OpenShift or a Red Hat-based distribution
  • Security audit requirements demand the minimal attack surface
  • You want to use crun as the default low-level runtime for faster startup

CRI-O is the specialist. It does one thing extremely well and nothing else. If you don’t need Docker API compatibility, general-purpose container management, or daemonless operation, CRI-O is the leanest option.

Choose Podman if:

  • You want a Docker replacement on your workstation or server
  • You need rootless containers for security compliance
  • You prefer systemd-native container management via Quadlet
  • You want to run container pods without Kubernetes
  • You’re a developer who wants the Docker CLI experience without the Docker daemon

Podman is the generalist for non-Kubernetes workloads. It covers the same ground that Docker does — image management, container lifecycle, Compose compatibility — but with a more secure, daemonless architecture.

Security Comparison

Rootless Execution

RuntimeRootless ModeImplementation
containerdNot supported nativelyRequires root for the daemon; containers run as the daemon user
CRI-OVia crun user namespacesKubernetes manages user namespace mapping through the CRI spec
PodmanNative, default modeUses Linux user namespaces (unshare --map-root-user) for full rootless isolation

Podman’s rootless support is the most mature. It uses subuid/subgid mappings to give each container its own uid range:

1
2
3
4
5
6
7
# Check your user namespace mapping
cat /etc/subuid
# youruser:100000:65536

# Run a container completely rootless
podman run --rm -it docker.io/library/alpine:latest whoami
# root (but this is mapped to your non-root host user)

Image Signing and Verification

All three runtimes support image verification, but the mechanisms differ:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# containerd: uses cosign for signature verification
ctr content fetch --all-platforms docker.io/library/nginx:latest
# Verification via external cosign tool

# CRI-O: built-in sigstore/cosign support
# /etc/crio/crio.conf
[crio.image]
signature_policy = "/etc/containers/policy.json"

# Podman: built-in signature verification
podman image trust set -t reject default
podman image trust set -t signedAccept registry.example.com

Performance Benchmarks

Based on publicly available benchmarks and community testing:

MetriccontainerdCRI-O + crunPodman + crun
Container start time~200ms~150ms~180ms
Memory overhead (daemon)~50MB~30MB0MB (daemonless)
Image pull throughputFastFastFast
Concurrent containers10,000+10,000+Limited by user session

CRI-O with crun tends to have the fastest container startup because it has the least abstraction layer between the kubelet and the OCI runtime. Podman’s daemonless model means zero baseline memory overhead — but each session has its own overhead, which can add up on systems running many isolated containers.

Migration Paths

From Docker to containerd

If you’re currently using Docker Engine and want to understand what’s underneath:

1
2
3
4
5
6
7
8
# Docker already uses containerd
docker info | grep -A2 "Container Runtime"
# Container Runtime: containerd

# To use containerd directly (without Docker):
sudo systemctl stop docker
sudo systemctl start containerd
# Use ctr or nerdctl for CLI operations

For a drop-in Docker CLI replacement that uses containerd underneath, consider nerdctl:

1
2
3
4
5
6
# Install nerdctl
sudo apt-get install -y nerdctl

# Works like docker but talks directly to containerd
nerdctl run -d -p 80:80 nginx:latest
nerdctl compose up -d  # Docker Compose compatible

From Docker to Podman

The migration is nearly seamless:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Install podman and podman-docker (provides docker CLI shim)
sudo apt-get install -y podman podman-docker

# Your existing docker commands work unchanged
docker run -d -p 80:80 nginx:latest
# Actually runs: podman run -d -p 80:80 nginx:latest

# Migrate existing Docker images
podman load -i docker-images.tar
# Or pull fresh from registry
podman pull docker.io/library/nginx:latest

Frequently Asked Questions

Can I use Podman with Kubernetes?

Yes, but not as a direct kubelet runtime. Podman supports podman play kube to deploy Kubernetes YAML files directly:

1
podman play kube my-deployment.yaml

For running Podman containers inside Kubernetes pods, you can use Podman as a runtime via the CRI-O-compatible podman-remote socket, but this is not a common production pattern. For Kubernetes clusters, containerd or CRI-O are the recommended choices.

Is containerd a replacement for Docker?

Not directly. containerd is the runtime that Docker uses internally. If you remove Docker, you lose the Docker CLI, Compose support, and image building. To replace Docker with containerd, you need additional tools like nerdctl for the CLI and BuildKit for image building. Podman is a more direct Docker replacement since it includes the CLI and build capabilities.

Which runtime is most secure for production?

For rootless operation, Podman is the clear winner — it supports rootless containers natively with minimal configuration. For Kubernetes workloads, CRI-O has the smallest attack surface because it implements only the CRI specification and nothing else. For general-purpose servers, containerd with proper namespace isolation and network policies provides enterprise-grade security.

Can I switch runtimes on an existing Kubernetes cluster?

Yes, but it requires draining nodes and changing the kubelet configuration. The process involves:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# On each node:
sudo systemctl stop containerd
sudo systemctl disable containerd

sudo systemctl start crio
sudo systemctl enable crio

# Update kubelet to point to the new runtime socket
sudo sed -i 's|--container-runtime-endpoint.*|--container-runtime-endpoint=unix:///var/run/crio/crio.sock|' /var/lib/kubelet/kubeadm-flags.env
sudo systemctl restart kubelet

Always test in a non-production environment first. Not all container images behave identically across runtimes.

Does CRI-O support Docker Compose files?

No. CRI-O is purpose-built for Kubernetes and does not support Docker Compose or any standalone container management. If you need Compose support alongside Kubernetes, pair CRI-O (for Kubernetes) with Podman or containerd+nerdctl (for standalone workloads) on the same node.

What is the difference between runc and crun?

Both are OCI-compliant low-level container runtimes. runc is the reference implementation written in Go, used by default in Docker and containerd. crun is a faster alternative written in C, with lower memory usage and faster startup times. CRI-O supports both, and Podman can use either. Switching to crun typically reduces container startup time by 20-30%.

How do I monitor container runtime performance?

For containerd, use the built-in metrics endpoint:

1
2
# containerd metrics (Prometheus format)
curl http://localhost:1338/v1/metrics

For CRI-O, use crictl stats:

1
2
3
crictl stats --all
# CONTAINER           CPU %   MEM     NET       BLOCK
# abc123              0.12    45MB    1.2KB/s   0B/s

For Podman, use podman stats:

1
2
3
podman stats --all
# ID            NAME        CPU %   MEM USAGE   MEM %   NET IO    BLOCK IO
# abc123        web         0.15    48.2MB      2.3%    1KB/0B    0B/0B

Summary

CriteriacontainerdCRI-OPodman
Best forKubernetes + general purposeKubernetes-onlyDevelopment + rootless production
ComplexityMediumLowLow-Medium
SecurityGood (daemonized)Excellent (minimal)Excellent (rootless)
KubernetesNative (default)Native (purpose-built)Limited (play kube)
Standalone containersVia nerdctlNot supportedNative
Learning curveModerateLowLow (Docker-compatible CLI)

For a complete container ecosystem, consider combining these with our guides on container build tools for image creation, container registries for image storage, and Kubernetes distributions for orchestration.

Advertise here