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”


🔥 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 Container Runtime Evolution - Docker vs ContainerD
The Container Runtime Evolution – Docker vs ContainerD

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):

Understanding the Architectural Difference - Docker Architecture (deprecated) - Docker vs ContainerD - thedevopstooling.com1
Understanding the Architectural Difference – Docker Architecture (deprecated) – Docker vs ContainerD

ContainerD Architecture (current standard):

Understanding the Architectural Difference - ContainerD Architecture (current standard) - Docker vs ContainerD - thedevopstooling.com
Understanding the Architectural Difference – ContainerD Architecture (current standard) – Docker vs ContainerD

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:

ComponentDockerContainerD
Exposed APIs2+ (Docker API + ContainerD CRI)1 (CRI only)
Default RootYes (Docker daemon runs as root)No (can run rootless)
Network Exposure3 ports (Docker API, Registry, Swarm)1 port (CRI socket)
Additional ServicesBuild system, Swarm, Volume pluginsContainer execution only
Process PrivilegesFull Docker daemon privilegesMinimal 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 VersionDefault RuntimeDocker Support
< 1.23DockerNative
1.24+ContainerDVia 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:

  1. ✅ You can identify the runtime being used in your cluster
  2. ✅ You understand the architectural differences between Docker and ContainerD
  3. ✅ You can explain why the migration happened
  4. ✅ 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 Commandcrictl EquivalentNotes
docker pscrictl psList containers
docker ps -acrictl ps -aList all containers
docker inspect <id>crictl inspect <id>Container details
docker logs <id>crictl logs <id>Container logs
docker exec -it <id> bashcrictl exec -it <id> bashExecute commands
docker imagescrictl imagesList images
docker pull <image>crictl pull <image>Pull images
docker infocrictl infoRuntime 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:

Practice challenges:

  1. Investigate: Run kubectl describe node on your cluster and identify which container runtime is being used
  2. Compare: If you have access to both Docker and ContainerD clusters, time how long it takes to start identical pods on each
  3. Debug: Practice using crictl commands 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

Similar Posts

One Comment

Leave a Reply