Skip to main content

Podman vs Docker vs containerd — Container Runtime Comparison

· 9 min read
Goel Academy
DevOps & Cloud Learning Hub

Docker made containers mainstream, but it is no longer the only way to run them. Kubernetes dropped Docker as a runtime in version 1.24. Podman offers a daemonless, rootless alternative with Docker CLI compatibility. containerd and CRI-O power most production Kubernetes clusters. The container ecosystem has matured beyond a single tool, and understanding the options helps you make the right choice for your specific use case.

The Container Runtime Landscape

The term "container runtime" is overloaded. There are high-level runtimes (Docker, Podman) that provide user-friendly CLIs and image management, and low-level runtimes (runc, crun) that actually create containers using Linux kernel features. In between sits containerd, which manages container lifecycle without the high-level tooling.

User-facing tools (high-level):
Docker CLI → dockerd → containerd → runc
Podman CLI → (no daemon) → conmon → runc/crun
nerdctl CLI → (no daemon) → containerd → runc

Kubernetes CRI:
kubelet → containerd → runc
kubelet → CRI-O → runc/crun

Every tool in this chain eventually calls a low-level OCI runtime (runc or crun) that uses Linux namespaces and cgroups to create the container. The differences are in what sits above that layer.

Docker Engine Architecture

Docker is actually a stack of components, not a single binary.

# The Docker stack:
# docker CLI → REST API → dockerd → containerd → runc

# docker (CLI) — translates user commands to API calls
which docker
# /usr/bin/docker

# dockerd — the Docker daemon, manages images, networks, volumes
ps aux | grep dockerd
# root 1234 /usr/bin/dockerd -H fd://

# containerd — container lifecycle management
ps aux | grep containerd
# root 1235 /usr/bin/containerd

# runc — the OCI runtime that creates containers
which runc
# /usr/bin/runc
# Check versions of all components
docker version
# Client: Docker Engine - Community
# Version: 24.0.7
# Server: Docker Engine - Community
# Version: 24.0.7
# containerd: 1.7.12
# runc: 1.1.12

# Docker daemon exposes a REST API
curl --unix-socket /var/run/docker.sock http://localhost/v1.44/containers/json | python3 -m json.tool

The important thing to understand: Docker already uses containerd internally. When Kubernetes "dropped Docker support," it dropped the dockerd layer — not containerd. The containers themselves run exactly the same way.

Podman — Daemonless and Rootless

Podman was designed by Red Hat as a direct Docker replacement. Its key differentiators: no daemon process, rootless by default, and a native pod concept.

# Install Podman
# Fedora/RHEL
sudo dnf install podman

# Ubuntu/Debian
sudo apt-get install podman

# macOS
brew install podman
podman machine init
podman machine start
# Podman CLI is intentionally Docker-compatible
podman run -d --name web -p 8080:80 nginx:alpine
podman ps
podman logs web
podman stop web
podman rm web

# Pull and manage images
podman pull docker.io/library/postgres:16-alpine
podman images
podman rmi nginx:alpine

# Build images (same Dockerfile, same syntax)
podman build -t myapp:latest .
# The key difference — no daemon
ps aux | grep podman
# Nothing! Podman does not run a background daemon

# Each container is a direct child process of the podman command
# When you run "podman run", it forks conmon (container monitor)
# which directly manages the container process

# This means:
# - No single point of failure (daemon crash does not kill all containers)
# - Containers survive podman upgrades
# - Lower memory footprint (no persistent daemon)

Podman Pods

Podman has a native pod concept — a group of containers sharing the same network namespace, similar to Kubernetes pods.

# Create a pod
podman pod create --name myapp -p 8080:80

# Add containers to the pod
podman run -d --pod myapp --name web nginx:alpine
podman run -d --pod myapp --name api myapp:latest

# Containers in the same pod share localhost
# web can reach api at localhost:3000
# api can reach web at localhost:80

# List pods
podman pod ls

# Generate Kubernetes YAML from a pod
podman generate kube myapp > myapp-k8s.yaml

# Deploy Kubernetes YAML with Podman
podman play kube myapp-k8s.yaml

containerd — The Industry Standard

containerd is the container runtime that both Docker and Kubernetes use under the hood. You can also use it directly through the ctr CLI or through nerdctl (a Docker-compatible CLI).

# containerd is typically already running if you have Docker
sudo systemctl status containerd

# The native CLI is ctr — functional but not user-friendly
sudo ctr images pull docker.io/library/nginx:alpine
sudo ctr run docker.io/library/nginx:alpine web
sudo ctr containers ls

# ctr is low-level and not meant for daily use
# Use nerdctl for a Docker-like experience
# Install nerdctl — Docker-compatible CLI for containerd
# Download from: https://github.com/containerd/nerdctl/releases

# nerdctl works like Docker but talks directly to containerd
nerdctl run -d --name web -p 8080:80 nginx:alpine
nerdctl ps
nerdctl logs web
nerdctl build -t myapp .
nerdctl compose up -d # Docker Compose compatible!
# containerd architecture:
# nerdctl/ctr → containerd (gRPC API) → runc
#
# No dockerd in the path — fewer moving parts
# containerd manages:
# - Image pulls and storage
# - Container lifecycle (create, start, stop, delete)
# - Snapshots (filesystem layers)
# - Task execution

CRI-O — Kubernetes-Focused

CRI-O is a minimal container runtime built specifically for Kubernetes. It implements the Container Runtime Interface (CRI) and nothing else — no CLI for running standalone containers, no image build capability.

# CRI-O is used exclusively by Kubernetes
# Install on a Kubernetes node
sudo apt-get install cri-o cri-o-runc

# Start CRI-O
sudo systemctl enable crio
sudo systemctl start crio

# CRI-O only responds to CRI gRPC calls from kubelet
# It does not have a user-facing CLI like docker or podman

# Use crictl to interact (Kubernetes CRI debugging tool)
sudo crictl ps
sudo crictl images
sudo crictl logs <container-id>

CRI-O's philosophy: do one thing well. It runs containers for Kubernetes and nothing else. This makes it smaller, more secure (less attack surface), and easier to audit.

Feature Comparison

FeatureDockerPodmancontainerd (nerdctl)CRI-O
DaemonYes (dockerd)No (daemonless)Yes (containerd)Yes (crio)
Root required (default)YesNo (rootless default)YesYes
Rootless modeSupported (opt-in)DefaultSupportedNot applicable
Kubernetes CRINo (removed in 1.24)No (uses CRI-O on RHEL)Yes (built-in)Yes (primary purpose)
Docker ComposeNativepodman-compose or socketnerdctl composeNo
Image builddocker buildpodman build (Buildah)nerdctl build (BuildKit)No
SwarmYesNoNoNo
Pod conceptNoYes (Kubernetes-like)Yes (nerdctl)Yes (Kubernetes)
Docker CLI compatibleN/A (is Docker)Yes (alias docker=podman)Yes (nerdctl)No (uses crictl)
OCI image supportYesYesYesYes
Systemd integrationsystemd servicepodman generate systemdsystemd servicesystemd service
macOS/WindowsDocker DesktopPodman machineNo (Linux only)No (Linux only)

Podman CLI Compatibility with Docker

Podman aims for full Docker CLI compatibility. Most Docker commands work by simply replacing docker with podman.

# Create a shell alias
alias docker=podman

# These all work identically:
docker run -d --name web nginx:alpine
docker ps
docker logs web
docker exec -it web sh
docker stop web
docker rm web
docker images
docker pull ubuntu:24.04
docker build -t myapp .
docker tag myapp:latest registry.example.com/myapp:latest
docker push registry.example.com/myapp:latest
# For tools that need a Docker socket, Podman can emulate one
# Enable the Podman socket (systemd user service)
systemctl --user enable podman.socket
systemctl --user start podman.socket

# Set DOCKER_HOST to the Podman socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock

# Now Docker Compose and other Docker API tools work with Podman
docker compose up -d # Actually uses Podman!

Known compatibility gaps:

# Docker Swarm commands do not work
podman swarm init # Error: unknown command "swarm"

# Docker network create (some drivers differ)
podman network create --driver macvlan mynet
# Some network drivers behave differently

# Docker plugins are not supported
podman plugin install # Error: unknown command "plugin"

Migrating from Docker to Podman

# Step 1: Install Podman alongside Docker
sudo apt-get install podman

# Step 2: Export existing Docker images
docker save myapp:latest | podman load

# Or pull directly from a registry
podman pull docker.io/myorg/myapp:latest

# Step 3: Transfer Docker volumes (if needed)
docker run --rm -v myvolume:/data -v $(pwd):/backup alpine \
tar czf /backup/volume-backup.tar.gz -C /data .

podman volume create myvolume
podman run --rm -v myvolume:/data -v $(pwd):/backup alpine \
tar xzf /backup/volume-backup.tar.gz -C /data

# Step 4: Update Compose files (minimal changes)
# Replace docker-compose with podman-compose
pip install podman-compose
podman-compose up -d

# Or use the Podman socket with docker compose
systemctl --user start podman.socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock
docker compose up -d
# Step 5: Update systemd services
# Docker:
# systemctl start docker
# docker run --restart=always myapp

# Podman:
podman run -d --name myapp myapp:latest
podman generate systemd --new --name myapp > ~/.config/systemd/user/myapp.service
systemctl --user enable myapp.service
systemctl --user start myapp.service

nerdctl for containerd

If you use containerd (common on Kubernetes nodes), nerdctl gives you a Docker-compatible CLI without installing Docker.

# nerdctl supports most Docker commands
nerdctl run -d --name web nginx:alpine
nerdctl ps
nerdctl logs web
nerdctl build -t myapp .
nerdctl compose up -d

# Unique nerdctl features:
# Image encryption
nerdctl image encrypt --recipient mykey myapp:latest myapp:encrypted

# Lazy pulling (stargz snapshotter)
nerdctl run --snapshotter stargz gcr.io/stargz-containers/nginx:latest

Choosing the Right Runtime

Decision tree:

Are you running Kubernetes?
├── Yes → containerd (most common) or CRI-O (RHEL/OpenShift)
└── No
├── Do you need Docker Swarm?
│ └── Yes → Docker
└── No
├── Is rootless/daemonless important?
│ └── Yes → Podman
└── No
├── Do you need Docker Compose?
│ ├── Yes → Docker or Podman (with socket)
│ └── No → Any runtime works
└── Are you on a team that knows Docker?
├── Yes → Docker (lowest friction)
└── No → Podman (better defaults)

For most individual developers and small teams, Docker remains the easiest choice — it has the largest ecosystem, most documentation, and best tooling integration. Podman is the right choice when security is a priority (rootless default, no daemon) or when you are in the Red Hat ecosystem. containerd and CRI-O are infrastructure-level tools — you interact with them through Kubernetes, not directly.

Wrapping Up

The container runtime landscape has evolved from "Docker is the only option" to a healthy ecosystem of specialized tools. Docker is still the most user-friendly choice for development and small-scale production. Podman offers better security defaults and daemon-free architecture. containerd is the engine under both Docker and Kubernetes. CRI-O is purpose-built for Kubernetes and nothing else. The good news: they all run OCI-standard containers, so your images work everywhere. Choose the runtime that matches your operational needs — security requirements, team expertise, and deployment target — not the one with the most blog posts about it.

This post wraps up the Docker series. From containers and images to networking, Compose, Dockerfiles, security, monitoring, orchestration with Swarm, and alternative runtimes — you now have a comprehensive understanding of the container ecosystem. The next step is to take these containerized applications and deploy them on Kubernetes, where orchestration happens at a much larger scale.