Kubernetes Beyond Docker: Migrating to containerd and What It Means for Your Clusters
On this page
- Understanding the Docker Deprecation
- The Architectural Shift Explained
- Impact Assessment: What This Means for Your Clusters
- Migration Guide: Moving from Docker to containerd
- Debugging and Troubleshooting containerd
- Adapting DevOps Practices for containerd
- Best Practices for containerd in Production
- Future-Proofing Your Container Strategy
- Conclusion: Embracing the Containerization Evolution
Kubernetes has deprecated and removed Docker support, but this doesn’t mean containers are going away. In fact, this architectural shift towards direct integration with containerd represents a significant evolution in container orchestration. This guide explores what this change means for your clusters, how to prepare, and why this transition ultimately creates a more stable and efficient Kubernetes ecosystem.
Understanding the Docker Deprecation
When Kubernetes announced the deprecation of Docker as a container runtime in version 1.20, it caused considerable confusion in the community. Headlines like “Kubernetes is dropping Docker support” led many to believe their Docker containers would no longer work with Kubernetes. This misunderstanding stems from conflating Docker (the full platform) with the underlying container technologies.
What Was Actually Deprecated?
To clarify what’s actually changing, we need to understand the components involved:
- Docker Engine: A complete platform for building, shipping, and running containers
- dockershim: A compatibility layer in Kubernetes that translated between Docker and Kubernetes’ Container Runtime Interface (CRI)
- containerd: The underlying container runtime that Docker itself uses
- OCI (Open Container Initiative): The standards that govern container formats and runtimes
What Kubernetes deprecated was dockershim - the adapter between Kubernetes and Docker - not support for Docker-formatted containers.
The Timeline of Change
Here’s how the deprecation process unfolded:
- Kubernetes 1.20 (December 2020): Official deprecation announced
- Kubernetes 1.22 (August 2021): Last version with dockershim included
- Kubernetes 1.24 (April 2022): dockershim removed from Kubernetes
This means any clusters running Kubernetes 1.24 or newer need an alternative container runtime to Docker, such as containerd, CRI-O, or another CRI-compliant runtime.
The Architectural Shift Explained
To understand why this change makes sense, let’s examine the evolution of container runtime architecture in Kubernetes.
The Old Docker-Based Architecture
In the original architecture, Kubernetes interacted with containers through multiple layers:
Kubernetes → dockershim → Docker Engine → containerd → runcThis architecture had several drawbacks:
- Inefficiency: Each additional layer adds overhead
- Complexity: More components means more potential failure points
- Maintenance burden: The Kubernetes team had to maintain dockershim
- Inconsistency: Docker wasn’t designed to be embedded in an orchestrator
The New CRI-Based Architecture
With the new architecture, Kubernetes communicates directly with the container runtime via the Container Runtime Interface (CRI):
Kubernetes → containerd → runcOr alternatively:
Kubernetes → CRI-O → runcThis simplified architecture offers several benefits:
- Improved performance: Fewer layers means better performance
- Simplified maintenance: Less code for the Kubernetes team to maintain
- Standardization: Any runtime implementing the CRI standard works seamlessly
- Future-proofing: Easier to adopt new container technologies as they emerge
Impact Assessment: What This Means for Your Clusters
The impact of this change varies depending on how you’re using Kubernetes. Let’s examine different scenarios:
For Most Users: Minimal Impact
If you’re using Kubernetes as a platform to deploy containerized applications, the impact is minimal:
- Docker-built images will continue to work without any changes
- Your Dockerfiles don’t need to be modified
- CI/CD pipelines that build Docker images remain valid
- Container registries and image distribution are unaffected
Potential Issues to Watch For
There are some specific scenarios where the removal of dockershim might cause issues:
- Direct node access using Docker CLI: If your scripts or operations rely on running
dockercommands directly on Kubernetes nodes, these will no longer work - Docker-specific features: If you’re using Docker-specific features not in the OCI spec, these might not work
- Docker socket mounting: Pods that mount the Docker socket (
/var/run/docker.sock) for Docker-in-Docker (DinD) operations - Custom scripts and tooling: Any custom operational scripts that assume Docker is present on nodes
Real-World Impact Example
Consider a common CI/CD workflow where images are built inside a pod:
Before (with Docker):
# Pod that builds images using Docker-in-Docker
apiVersion: v1
kind: Pod
metadata:
name: image-builder
spec:
containers:
- name: docker
image: docker:dind
securityContext:
privileged: true
volumeMounts:
- name: docker-socket
mountPath: /var/run/docker.sock
volumes:
- name: docker-socket
hostPath:
path: /var/run/docker.sockAfter (without Docker): This approach no longer works on containerd-based clusters. Instead, you would use alternatives like:
# Pod that builds images using Kaniko (no Docker required)
apiVersion: v1
kind: Pod
metadata:
name: kaniko-builder
spec:
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:latest
args:
- "--dockerfile=Dockerfile"
- "--context=git://github.com/user/repo"
- "--destination=user/image:tag"
volumeMounts:
- name: kaniko-secret
mountPath: /kaniko/.docker
volumes:
- name: kaniko-secret
secret:
secretName: docker-registry-credentials
items:
- key: .dockerconfigjson
path: config.jsonMigration Guide: Moving from Docker to containerd
If you’re running clusters that still use Docker via dockershim, here’s a practical guide to migrating to containerd.
Step 1: Identify Current Runtime Configuration
First, determine which container runtime your clusters are using:
kubectl get nodes -o wideLook for the “CONTAINER-RUNTIME” column. If it shows “docker”, you’re using Docker.
For more detailed information:
kubectl get nodes -o=jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.nodeInfo.containerRuntimeVersion}{"\n"}{end}'Step 2: Assess Compatibility
Before migrating, check for compatibility issues:
Identify Docker socket mounts:
bashkubectl get pods --all-namespaces -o json | jq '.items[] | select(.spec.volumes[]?.hostPath.path == "/var/run/docker.sock") | .metadata.namespace + "/" + .metadata.name'Identify Docker-in-Docker containers:
bashkubectl get pods --all-namespaces -o json | jq '.items[] | select(.spec.containers[].image | contains("docker")) | .metadata.namespace + "/" + .metadata.name'Check for Docker volume drivers that might not be supported with containerd.
Step 3: Plan Upgrade Path
Choose one of these migration approaches:
- Rolling upgrade: Upgrade one node at a time, moving workloads off the node during upgrade
- Blue/green deployment: Create a new node pool with containerd, then migrate workloads
- Replacement: Build new clusters with containerd and migrate applications
For managed Kubernetes services, check their documentation for specific migration paths:
- AWS EKS: Migrating from dockershim to containerd
- Google GKE: GKE dockershim deprecation
- Azure AKS: AKS containerd support
Step 4: Configure containerd
When migrating to containerd, you’ll need to ensure it’s properly configured:
Sample containerd configuration (/etc/containerd/config.toml):
version = 2
[plugins]
[plugins."io.containerd.grpc.v1.cri"]
[plugins."io.containerd.grpc.v1.cri".containerd]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[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
[plugins."io.containerd.grpc.v1.cri".registry]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = ["https://registry-1.docker.io"]The key settings to configure include:
- Registry mirrors for improved image pulling
- Runtime handlers for different container types
- Logging and metrics collection
- Cgroup management strategy
Step 5: Upgrade Nodes
The exact process will depend on your environment. Here’s a general approach for self-managed clusters:
Drain the node to move workloads:
bashkubectl drain node-name --ignore-daemonsetsInstall containerd:
bashapt-get update && apt-get install -y containerd.ioConfigure kubelet to use containerd: Edit
/var/lib/kubelet/kubeadm-flags.envto include:KUBELET_EXTRA_ARGS=--container-runtime=remote --container-runtime-endpoint=unix:///run/containerd/containerd.sockRestart kubelet:
bashsystemctl restart kubeletUncordon the node to allow workloads to return:
bashkubectl uncordon node-name
Step 6: Validate Upgrade
After upgrading, verify everything is working correctly:
Check node status:
bashkubectl get nodes -o wideVerify pods are running:
bashkubectl get pods --all-namespacesTest a deployment:
bashkubectl create deployment nginx --image=nginx kubectl expose deployment nginx --port=80 kubectl get pods,svc -l app=nginx
Debugging and Troubleshooting containerd
As you transition from Docker to containerd, you’ll need to adapt your troubleshooting techniques.
Command Line Tools for containerd
Instead of the Docker CLI, you’ll use these tools:
crictl: A command-line interface for CRI-compatible container runtimes
bash# List containers crictl ps # Get container info crictl inspect container-id # View container logs crictl logs container-id # Execute command in container crictl exec -it container-id sh # List images crictl imagesctr: A lower-level CLI for direct containerd operations
bash# List containers ctr containers ls # List images ctr images ls # Pull an image ctr images pull docker.io/library/nginx:latest
Docker to containerd Command Mapping
| Docker Command | containerd Equivalent (crictl) |
|---|---|
docker ps | crictl ps |
docker logs | crictl logs |
docker exec | crictl exec |
docker images | crictl images |
docker pull | crictl pull |
docker inspect | crictl inspect |
docker stats | crictl stats |
Common Issues and Solutions
Issue 1: Image Pull Failures
Symptoms: Pods stuck in ImagePullBackOff status
Troubleshooting:
- Check image pull status:bash
crictl images - Try pulling manually:bash
crictl pull problematic-image:tag - Check registry configuration in containerd:bash
cat /etc/containerd/config.toml | grep registry -A 10
Solution: Ensure proper registry configuration in containerd’s config.toml, including authentication for private registries.
Issue 2: Container Startup Failures
Symptoms: Pods stuck in CrashLoopBackOff status
Troubleshooting:
- Get container IDs:bash
crictl ps -a - Check container logs:bash
crictl logs container-id - Inspect container details:bash
crictl inspect container-id
Solution: Address the application error shown in logs or fix container configuration issues.
Issue 3: containerd Service Issues
Symptoms: kubelet logs show containerd connection failures
Troubleshooting:
- Check containerd status:bash
systemctl status containerd - View containerd logs:bash
journalctl -u containerd
Solution: Restart containerd or fix configuration issues based on error messages.
Adapting DevOps Practices for containerd
The transition to containerd requires adapting your DevOps practices, particularly around CI/CD pipelines, monitoring, and debugging.
CI/CD Pipeline Adjustments
If your pipelines rely on Docker-in-Docker, consider these alternatives:
Kaniko: Build images inside Kubernetes without Docker
yaml# Example Kaniko job apiVersion: batch/v1 kind: Job metadata: name: kaniko-build spec: template: spec: containers: - name: kaniko image: gcr.io/kaniko-project/executor:latest args: - "--dockerfile=Dockerfile" - "--context=git://github.com/user/repo" - "--destination=user/image:tag" restartPolicy: NeverBuildKit: Advanced image building with efficient caching
yaml# Example BuildKit deployment apiVersion: apps/v1 kind: Deployment metadata: name: buildkitd spec: replicas: 1 selector: matchLabels: app: buildkitd template: metadata: labels: app: buildkitd spec: containers: - name: buildkitd image: moby/buildkit:latest securityContext: privileged: trueExternal builders: Use external CI systems like GitHub Actions, GitLab CI, or Jenkins for building images
Monitoring and Logging Adaptations
Update your monitoring and logging practices:
Metrics collection: Configure Prometheus to scrape containerd metrics:
yaml# Prometheus ServiceMonitor for containerd apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: containerd namespace: monitoring spec: endpoints: - interval: 30s port: metrics namespaceSelector: matchNames: - kube-system selector: matchLabels: k8s-app: containerdLog collection: Update log collection configurations to capture containerd logs:
yaml# Fluentd ConfigMap snippet for containerd logs apiVersion: v1 kind: ConfigMap metadata: name: fluentd-config data: containerd.conf: | <source> @type tail path /var/log/containers/*.log pos_file /var/log/containerd.log.pos tag containerd.* <parse> @type json time_key time time_format %Y-%m-%dT%H:%M:%S.%NZ </parse> </source>
Security Considerations
Adjust your security posture for containerd:
Runtime security: Update seccomp and AppArmor profiles for containerd
Privilege settings: Review pod security contexts and container capabilities
Image scanning: Ensure your image scanning solutions work with containerd
Best Practices for containerd in Production
To ensure optimal performance and reliability with containerd, follow these best practices:
Performance Optimization
Memory management:
toml# In config.toml [plugins."io.containerd.grpc.v1.cri".containerd] snapshotter = "overlayfs" disable_snapshot_annotations = trueImage pulling optimization:
toml# In config.toml [plugins."io.containerd.grpc.v1.cri".registry] [plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://registry-1.docker.io", "https://your-mirror.example.com"]Resource limits: Configure proper resource limits for containerd service:
systemd# In containerd.service [Service] CPUAccounting=true MemoryAccounting=true CPUQuota=200% MemoryLimit=2G
Stability and Reliability
Proper shutdown handling:
toml# In config.toml [plugins."io.containerd.internal.v1.shutdown"] timeout = "30s"Health checking: Implement regular health checks
bash# Systemd service check ExecStartPost=/bin/sh -c 'while ! ctr version; do sleep 1; done'Failure recovery: Ensure automatic restart on failure
systemd# In containerd.service [Service] Restart=always RestartSec=5
Backup and Disaster Recovery
State backup: Back up containerd’s state directory:
bashtar -czf containerd-backup.tar.gz /var/lib/containerdConfiguration backup: Version control your containerd configurations
Recovery testing: Regularly test restoration procedures
Future-Proofing Your Container Strategy
As container technologies continue to evolve, here are strategies to future-proof your approach:
Embrace OCI Standards
The Open Container Initiative (OCI) provides industry standards for container formats and runtimes. By adhering to these standards, you ensure compatibility regardless of the underlying runtime:
- Use OCI-compliant images: Standard Docker images already conform to OCI specifications
- Avoid runtime-specific features: Stick to features available across runtimes
- Leverage OCI hooks: For custom functionality that works across runtimes
Explore Advanced Container Runtimes
Beyond containerd, consider exploring specialized runtimes for specific use cases:
gVisor: For enhanced isolation and security
yaml# Pod using gVisor runtime apiVersion: v1 kind: Pod metadata: name: secure-pod spec: runtimeClassName: gvisor containers: - name: app image: my-secure-app:latestKata Containers: For hardware-level isolation
yaml# Pod using Kata Containers runtime apiVersion: v1 kind: Pod metadata: name: isolated-pod spec: runtimeClassName: kata containers: - name: app image: my-app:latest
Consider Specialized Build Tools
As containers evolve, specialized build tools offer advantages over traditional approaches:
- BuildPacks: For standardized, secure builds across languages
- ko: For building Go applications directly to container images
- Jib: For building Java containers without Dockerfiles
Conclusion: Embracing the Containerization Evolution
The transition from Docker to containerd in Kubernetes represents a natural evolution in container orchestration. By removing the dockershim layer, Kubernetes becomes more efficient, more stable, and better prepared for future innovations in containerization.
This change brings several key benefits:
- Simplified architecture: Fewer moving parts means fewer points of failure
- Improved performance: Direct integration with containerd reduces overhead
- Enhanced security: Less code means a smaller attack surface
- Better standards alignment: Kubernetes now works directly with OCI-compliant runtimes
- Future-proofing: The CRI abstraction makes it easier to adopt new container technologies
For most teams, this transition should be relatively straightforward, especially for workloads that don’t directly interact with the Docker socket. By following the migration guide and best practices outlined in this article, you can ensure a smooth transition to containerd and position your container strategy for long-term success.
Remember that Docker remains an excellent tool for local development and building images. The change in Kubernetes simply reflects the maturation of the container ecosystem, where specialized components can now efficiently handle different aspects of the container lifecycle.
As you adapt to this change, focus on building expertise with containerd and standardizing on OCI-compliant workflows. This approach will ensure your container strategy remains robust and adaptable in the ever-evolving landscape of cloud-native technologies.
The information in this article is based on Kubernetes 1.24 and later versions. Always refer to the official Kubernetes documentation for the most up-to-date information regarding container runtime support.