Docker vs ContainerD: The Ultimate Showdown and Why It Matters in 2025
Post 3 of 70 in the series “Mastering Kubernetes: A Practical Journey from Beginner to CKA”
Table of Contents
🔥 TL;DR
- Docker was removed as the default container runtime in Kubernetes 1.24+ due to architectural complexity
- ContainerD is now the standard runtime – it’s lighter, faster, and more secure than Docker
- Your existing container images work unchanged – only the runtime engine differs
- Migration requires updating cluster configuration, not rebuilding applications
- ContainerD provides better resource efficiency and tighter Kubernetes integration
Introduction: Docker vs ContainerD
Imagine you’ve been driving a powerful but gas-guzzling SUV (Docker) for years, and suddenly the highway authority announces that only efficient electric vehicles (ContainerD) are allowed on the main routes. Your destination hasn’t changed, your cargo stays the same, but you need to understand why this switch happened and what it means for your journey.
What we’ll learn today:
- Why Kubernetes deprecated Docker as a container runtime
- How ContainerD differs from Docker in architecture and performance
- Practical migration strategies for existing clusters
- Real-world implications for development and production workflows
Why this matters: This isn’t just a technical curiosity – it’s a fundamental shift affecting every Kubernetes deployment. Companies like Netflix, Shopify, and thousands of others had to navigate this transition. Understanding these changes helps you make informed decisions about cluster architecture, troubleshooting runtime issues, and planning future infrastructure. By the end, you’ll confidently explain this evolution to your team and implement the right runtime strategy.
Series context: In our previous post, we explored Kubernetes cluster architecture and how master nodes coordinate with worker nodes. Today we’re diving deeper into one of the most critical worker node components – the container runtime – and understanding a major evolution that reshaped the entire ecosystem.
Prerequisites
What you need to know:
- Understanding of containers and their lifecycle
- Basic Kubernetes architecture (covered in Post #2)
- Familiarity with kubectl commands
📌 Quick Refresher: A container runtime is the software responsible for running containers. It pulls images, creates containers, and manages their lifecycle. Think of it as the engine that actually executes your containerized applications.
Tools required:
- Access to a Kubernetes cluster (any version)
- kubectl configured and working
- Text editor for examining configurations
Previous posts to read:
- Post #2: Kubernetes Architecture Explained (essential for understanding where container runtimes fit)
Estimated time: 20-25 minutes to read and understand the concepts, plus hands-on exploration
Step-by-Step Tutorial
Theory First: The Container Runtime Evolution
The relationship between Kubernetes and Docker wasn’t always complicated. In the early days, Docker was the only game in town. But as Kubernetes matured, this tight coupling became problematic.

The Original Problem:
# Kubernetes 1.23 and earlier - Docker integration
apiVersion: v1
kind: Node
spec:
# Docker required multiple layers:
# kubectl → kubelet → dockershim → Docker daemon → containerd → runc
runtime: docker
Why was Docker problematic for Kubernetes?
Docker wasn’t designed specifically for Kubernetes. It included many features Kubernetes didn’t need (like the Docker CLI, build system, and volume management), creating unnecessary complexity and resource overhead.
Step 1: Understanding the Architectural Difference
Let’s examine how each runtime integrates with Kubernetes:
Docker Architecture (deprecated):

ContainerD Architecture (current standard):

What is runc and why does it appear in both architectures?
runc is the low-level container executor that actually creates and runs containers according to OCI specifications. Both Docker and ContainerD use runc as their final execution layer – it’s the industry standard for container creation.
💡 Pro Tip: ContainerD was actually part of Docker originally! Docker donated it to the CNCF, and now it’s the foundation that both Docker and Kubernetes build upon.
Step 2: Security Comparison – Attack Surface Analysis
Here’s why ContainerD is considered more secure for Kubernetes deployments:
| Component | Docker | ContainerD |
|---|---|---|
| Exposed APIs | 2+ (Docker API + ContainerD CRI) | 1 (CRI only) |
| Default Root | Yes (Docker daemon runs as root) | No (can run rootless) |
| Network Exposure | 3 ports (Docker API, Registry, Swarm) | 1 port (CRI socket) |
| Additional Services | Build system, Swarm, Volume plugins | Container execution only |
| Process Privileges | Full Docker daemon privileges | Minimal runtime privileges |
⚠️ Security Alert: Docker’s broader attack surface includes features like the Docker API socket, which if compromised, grants full container host access. ContainerD’s focused scope reduces these risks significantly.
Step 3: Examining Runtime Configuration in Real Clusters
Let’s explore how to identify and configure container runtimes:
Check your current runtime:
# Method 1: Check node information
kubectl get nodes -o wide
# Expected output shows CONTAINER-RUNTIME column:
# NAME STATUS VERSION CONTAINER-RUNTIME
# worker-1 Ready v1.28.0 containerd://1.7.0
# worker-2 Ready v1.28.0 containerd://1.7.0
# Method 2: Detailed node inspection
kubectl describe node <node-name> | grep "Container Runtime"
# Container Runtime Version: containerd://1.7.0
Verify runtime socket:
# On worker nodes, check the runtime socket
sudo ls -la /run/containerd/containerd.sock # ContainerD
sudo ls -la /var/run/docker.sock # Docker (if present)
# Check kubelet configuration
sudo cat /var/lib/kubelet/config.yaml | grep containerRuntime
Step 4: Understanding CRI (Container Runtime Interface)
In Kubernetes, ContainerD (a Container Runtime Interface that standardizes how Kubernetes communicates with different runtimes) enables this flexibility:
# CRI configuration example
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
containerRuntimeEndpoint: unix:///run/containerd/containerd.sock
What is the Container Runtime Interface (CRI)?
CRI is a plugin interface that allows Kubernetes to work with different container runtimes without changing core Kubernetes code. It defines standard APIs for container and image management.
❓ Check Understanding:
- Q: Which component directly communicates with the container runtime? (Answer: kubelet)
- Q: What eliminated the need for dockershim? (Answer: CRI implementation in ContainerD)
- Q: Do your container images need to change when switching runtimes? (Answer: No, images are OCI-compliant)
Step 5: Performance and Resource Impact Analysis
Let’s examine the real-world differences with concrete benchmarks:
Version Compatibility Matrix:
| K8s Version | Default Runtime | Docker Support |
|---|---|---|
| < 1.23 | Docker | Native |
| 1.24+ | ContainerD | Via cri-dockerd |
Resource Usage Comparison:
# ContainerD cluster resource usage
kubectl top nodes
# Typical resource usage per node:
# ContainerD: 100-200MB memory, 0.1-0.2 CPU cores
# Docker: 200-400MB memory, 0.2-0.4 CPU cores
# Check runtime processes
ps aux | grep containerd
ps aux | grep dockerd # If Docker is present
Container Startup Performance Benchmark:
# Performance benchmark using hyperfine
hyperfine --warmup 3 \
"kubectl run test-containerd --image=nginx:alpine --restart=Never --rm" \
"kubectl run test-docker --image=nginx:alpine --restart=Never --rm"
# Typical results:
# ContainerD: 2.5-3.2 seconds average
# Docker: 3.2-4.1 seconds average (20-30% slower)
Container Startup Performance:
# Performance test: Create a simple pod
apiVersion: v1
kind: Pod
metadata:
name: runtime-test
spec:
containers:
- name: nginx
image: nginx:alpine
resources:
requests:
memory: "64Mi"
cpu: "50m"
# Time the pod creation
time kubectl apply -f runtime-test.yaml
kubectl get pods -w # Watch startup time
# ContainerD typically shows 20-30% faster startup times
Step 6: Migration Strategies and Considerations
Pre-Migration Checklist:
- [ ] Verify cluster health with `kubectl get nodes`
- [ ] Backup etcd data: `etcdctl snapshot save`
- [ ] Document current runtime configs in `/var/lib/kubelet/`
- [ ] Test migration procedure on non-production nodes first
- [ ] Verify CNI plugin compatibility with ContainerD
- [ ] Update monitoring systems to use ContainerD metrics
- [ ] Plan maintenance windows for each node
ℹ️ Network Impact Note: ContainerD uses different CNI plugins than Docker’s legacy networking. Verify your current CNI (Calico, Flannel, Weave) supports ContainerD before migration.
For New Clusters:
# kubeadm with ContainerD (default in 1.24+)
sudo kubeadm init --cri-socket unix:///run/containerd/containerd.sock
# Verify ContainerD configuration
sudo containerd config default | sudo tee /etc/containerd/config.toml
For Existing Clusters (Migration Path):
# 1. Drain nodes one by one
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data
# 2. On the node, install and configure ContainerD
sudo apt-get update && sudo apt-get install -y containerd.io
# 3. Configure ContainerD
sudo mkdir -p /etc/containerd
sudo containerd config default | sudo tee /etc/containerd/config.toml
# 4. Update kubelet to use ContainerD (modern approach)
sudo kubeadm upgrade node --cri-socket unix:///run/containerd/containerd.sock
# 5. Restart services
sudo systemctl daemon-reload
sudo systemctl restart kubelet containerd
# 6. Uncordon the node
kubectl uncordon <node-name>
Rollback Procedure (if needed):
# Emergency rollback steps
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data
sudo systemctl stop kubelet containerd
sudo apt-get install -y docker-ce docker-ce-cli
sudo systemctl start docker
sudo kubeadm upgrade node --cri-socket unix:///var/run/docker.sock
sudo systemctl restart kubelet
kubectl uncordon <node-name>
⚠️ Production Alert: Always test migration procedures in development environments first. Plan for temporary node unavailability during the migration process.
Verification Steps:
- ✅ You can identify the runtime being used in your cluster
- ✅ You understand the architectural differences between Docker and ContainerD
- ✅ You can explain why the migration happened
- ✅ You know how to plan a runtime migration
Real-World Scenarios
Scenario 1: Enterprise Migration at Scale
Netflix’s Runtime Migration: Netflix migrated over 1,000 Kubernetes nodes from Docker to ContainerD during 2022-2023:
# Their migration strategy:
Phase 1: New clusters with ContainerD (3 months)
Phase 2: Non-critical workload migration (6 months)
Phase 3: Production workload migration (12 months)
# Results they reported:
- 15% reduction in node memory usage
- 25% faster container startup times
- 40% reduction in runtime-related incidents
- Simplified troubleshooting workflows
Migration considerations for large enterprises:
- Blue-green cluster strategy: Run parallel clusters during transition
- Monitoring integration: Update monitoring tools to work with ContainerD APIs
- CI/CD pipeline updates: Ensure build systems work with new runtime
- Developer training: Team education on new debugging procedures
Scenario 2: Development Workflow Impact
Before (Docker era):
# Developers could debug directly with Docker commands
docker ps # See running containers
docker logs <container-id> # View logs
docker exec -it <id> bash # Debug inside containers
After (ContainerD era):
# New debugging workflow with ContainerD tools
sudo crictl ps # See running containers
sudo crictl logs <id> # View logs
sudo crictl exec -it <id> bash # Debug inside containers
# Or use kubectl (recommended)
kubectl get pods
kubectl logs <pod-name>
kubectl exec -it <pod-name> -- bash
Command Cheat Sheet – Docker vs crictl:
| Docker Command | crictl Equivalent | Notes |
|---|---|---|
docker ps | crictl ps | List containers |
docker ps -a | crictl ps -a | List all containers |
docker inspect <id> | crictl inspect <id> | Container details |
docker logs <id> | crictl logs <id> | Container logs |
docker exec -it <id> bash | crictl exec -it <id> bash | Execute commands |
docker images | crictl images | List images |
docker pull <image> | crictl pull <image> | Pull images |
docker info | crictl info | Runtime information |
For a deeper dive into Docker commands, don’t miss our complete Docker Commands Cheat Sheet — a handy reference for everyday container operations.
Log Format Differences:
Docker Logs Format:
{"log":"Hello World\n","stream":"stdout","time":"2023-12-01T10:30:45.123456789Z"}
ContainerD Logs Format:
2023-12-01T10:30:45.123456789Z stdout F Hello World
Best practices for development teams:
- Use kubectl primarily: It works consistently across all runtimes
- Install crictl for debugging: When kubectl isn’t sufficient
- Update local tooling: Ensure development tools support ContainerD
- Container image building: Use buildah, podman, or Docker (still works for building)
⚠️ Warning: Never use docker commands directly on Kubernetes worker nodes with ContainerD – they won’t show Kubernetes-managed containers!
Common mistakes during transition:
- Assuming Docker CLI commands still work for debugging Kubernetes containers
- Not updating monitoring systems to use ContainerD metrics endpoints
- Forgetting to update CI/CD pipelines that relied on Docker daemon socket access
- Not training operations teams on crictl debugging procedures
- Overlooking CNI plugin compatibility during migration planning
Troubleshooting Tips
Common Error 1: “container runtime not running”
Issue: kubelet fails to start after migration Solution:
# Check ContainerD service status
sudo systemctl status containerd
# If not running, start it
sudo systemctl start containerd
sudo systemctl enable containerd
# Verify socket exists
ls -la /run/containerd/containerd.sock
Common Error 2: Images not pulling after migration
Issue: Pods stuck in “ImagePullBackOff” state Solution:
# Check ContainerD configuration
sudo cat /etc/containerd/config.toml | grep sandbox_image
# Restart ContainerD with proper configuration
sudo systemctl restart containerd
# Check kubelet can communicate with runtime
sudo journalctl -u kubelet -f
Common Error 3: Performance degradation after migration
Issue: Slower container startup or higher resource usage Solution:
# Optimize ContainerD configuration
sudo vim /etc/containerd/config.toml
# Enable optimizations (check your init system first):
# Enable optimizations (check your init system first):
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "runc"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true # For systemd-based systems
# SystemdCgroup = false # For non-systemd systems
# Restart ContainerD
sudo systemctl restart containerd
Debug Commands:
# Essential ContainerD debugging commands
sudo crictl info # Runtime information
sudo crictl images # List images
sudo crictl ps -a # List all containers
sudo ctr --namespace k8s.io containers list # Alternative container listing
# Health checks
kubectl get --raw='/readyz?verbose' # Control plane health
kubectl get nodes -o wide # Node and runtime status
Where to get help:
Next Steps
What’s coming next: In Post #4, we’ll explore “ETCD For Beginners: The Cluster’s Memory System.” You’ll discover how Kubernetes stores and manages all cluster data in this critical component, building on your understanding of how ContainerD manages actual container processes while etcd manages cluster state.
Additional learning:
- Experiment with crictl commands for container debugging
- Explore OCI (Open Container Initiative) specifications that both Docker and ContainerD implement
Practice challenges:
- Investigate: Run
kubectl describe nodeon your cluster and identify which container runtime is being used - Compare: If you have access to both Docker and ContainerD clusters, time how long it takes to start identical pods on each
- Debug: Practice using
crictlcommands to inspect running containers in a ContainerD cluster
Community engagement: Share your runtime migration experiences! Did you encounter any unexpected challenges? What tools did you find most helpful during the transition? Your insights help fellow practitioners navigate this important evolution.
FAQ Section
Do I need to rebuild my container images when switching from Docker to ContainerD?
No, container images remain fully compatible. Both runtimes implement the OCI (Open Container Initiative) specification, so existing images work without modification.
Can I still use Docker for building images if my cluster uses ContainerD?
Absolutely! Docker remains an excellent tool for building container images. The change only affects the runtime that executes containers in your Kubernetes cluster.
What happens to my existing pods during a runtime migration?
Existing pods need to be recreated with the new runtime. This is why migration typically involves draining nodes, updating the runtime, and allowing Kubernetes to reschedule pods.
Is ContainerD more secure than Docker for Kubernetes?
Yes, ContainerD has a smaller attack surface since it focuses only on container execution, unlike Docker which includes ContainerD plus additional services (buildkit, swarm orchestration, volume plugins, and Docker API) that aren’t needed for Kubernetes operations.
How do I debug containers without Docker CLI commands?
Use kubectl for most debugging tasks (kubectl logs, kubectl exec), and crictl for lower-level container runtime debugging when needed.
🔗 Series Navigation
Previous: Post #2 – Kubernetes Architecture Explained: Master vs Worker Nodes in Action
Next: Post #4 – ETCD For Beginners: The Cluster’s Memory System
Progress: You’re now 4% through the Kubernetes Fundamentals series! 🎉
💡 Pro Tip: Save this post as a reference for runtime migration planning. As we explore more Kubernetes components in upcoming posts, you’ll see how ContainerD’s efficiency impacts overall cluster performance and reliability.
📧 Never miss an update: Subscribe to get notified when new posts in this series are published. Next up: we’re diving into etcd, the critical data store that makes everything we’ve learned so far possible!
Tags: kubernetes, containerd, docker, container-runtime, migration, cri, devops, cka-prep

One Comment