A containerd 2.0 migration is now mandatory for any self-managed Kubernetes cluster that wants to move past v1.35. Kubernetes 1.35 is the last minor release that supports containerd 1.x, and upstream containerd 1.7 reached the end of extended support on 30 September 2026.
The upgrade looks like a simple package bump. It is not. containerd 2.0 removes the CRI v1alpha2 API, the runtime v1 shims and the AUFS snapshotter, disables Docker Schema 1 image pulls, and introduces config version 3 with a split CRI plugin.
This guide shows how to audit your nodes and images, convert config.toml to version 3, move registry mirrors to hosts.toml, and roll containerd 2.x across a cluster without breaking a single pod.
Table of Contents
- Technical Overview & Background
- What Breaks in containerd 2.0
- Core System Prerequisites
- Step-by-Step containerd 2.0 Migration
- Architecture & Component Breakdown
- Real-World Use Cases & Enterprise Examples
- Troubleshooting, Edge Cases & Best Practices
- FAQ
- Summary & Key Takeaways
Technical Overview & Background
containerd is the CNCF-graduated container runtime that sits between the kubelet and the OCI runtime (runc or crun). It pulls images, manages snapshots, and creates pod sandboxes through its built-in CRI plugin.
containerd 2.0 shipped in November 2024. It was the first major release since 1.0 in 2017, so it used the opportunity to remove years of deprecated code.
The support timeline that forces the move
| Component | Status (October 2026) |
|---|---|
| containerd 1.6 (LTS) | End of life |
| containerd 1.7 (LTS) | Extended support ended 30 Sep 2026 |
| containerd 2.0 (LTS) | Extended support until 1 Mar 2027 |
| containerd 2.2 | Supported until 6 Nov 2026 |
| containerd 2.3 (LTS) | Supported until 30 Apr 2028 |
| containerd 2.4 | Supported until May 2027 |
| Kubernetes 1.35 | Last minor release that supports containerd 1.x |
| Kubernetes 1.38 | Kubelet’s legacy cgroup-driver fallback for older runtimes is due for removal |
For most production fleets, containerd 2.3 LTS is the sensible target for a containerd 2.0 migration. It gives the longest support window, and you skip a second migration in early 2027.
Why Kubernetes cares about the runtime version
Since Kubernetes 1.34, the kubelet reads the cgroup driver directly from the CRI runtime instead of relying on its own cgroupDriver setting. containerd 2.x implements that RuntimeConfig call; containerd 1.x does not.
The kubelet also exposes a kubelet_cri_losing_support metric that flags nodes whose runtime is about to fall out of support. If that metric fires in your monitoring, the node is on borrowed time.
Managed Kubernetes is already moving
- Google GKE moves Linux nodes to containerd 2.0 with GKE 1.33 and Windows nodes with GKE 1.35. Automatic upgrades pause when GKE detects deprecated usage on your cluster.
- Amazon EKS and Azure AKS ship containerd 2.x in their current node images, so managed node groups follow the image.
Self-managed clusters (kubeadm, k3s, RKE2, bare metal and custom AMIs) get no such help. You own every node and every step of the containerd 2.0 migration.
What Breaks in containerd 2.0
Read this section before you start a containerd 2.0 migration on any production node. Every item below has caused real outages during upgrades.
Removed features
- CRI v1alpha2 API. Only CRI v1 remains. Old
crictlbuilds, legacy log shippers and security agents that talk v1alpha2 to the socket will fail. - Runtime v1 shims.
io.containerd.runtime.v1.linuxandio.containerd.runc.v1are gone. Useio.containerd.runc.v2. - AUFS snapshotter. Use
overlayfs. containerd.io/restart.logpathlabel. Replaced bycontainerd.io/restart.loguri.cri-containerd-*.tar.gzbundles. Install containerd, runc and CNI plugins separately.LimitNOFILEin the reference systemd unit. The default systemd value now applies. On systemd older than 240, setLimitNOFILE=1024:524288.io_uringsyscalls in the default seccomp profile.io_uring_setup,io_uring_enterandio_uring_registerare now blocked by default.
Disabled by default
- Docker Schema 1 image pulls. These images fail to pull unless you set
CONTAINERD_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE=1. Treat that variable as a short-lived escape hatch, not a fix.
Deprecated configuration
[plugins."io.containerd.grpc.v1.cri".registry]mirrorsandconfigs→ move toconfig_pathwith per-registryhosts.tomlfilesauths(static registry credentials) → move to KubernetesimagePullSecretsplugin_dirGo plugins → proxy or binary external plugins
GKE’s documentation notes that registry.auths, registry.configs and registry.mirrors are scheduled for removal in containerd 2.4 or later. Migrate them now while version 2.x still converts them.
Behaviour changes that surprise people
- NRI and CDI are enabled by default. Node Resource Interface plugins and Container Device Interface devices now load without extra config.
- Unprivileged ports and ICMP are enabled by default for CRI containers that do not use the host network or user namespaces. Set
enable_unprivileged_ports = falseandenable_unprivileged_icmp = falseif your security baseline forbids it. - Sandboxed CRI is the default pod-sandbox implementation, backed by the now-stable Sandbox API.
Core System Prerequisites
Confirm these before you start a containerd 2.0 migration:
- Kubernetes control plane: v1.30 or later during the transition (all currently supported versions work with containerd 2.x)
- OCI runtime: runc 1.2.0+ (required for CRI user namespaces) or a current crun
- CNI plugins: current release of the reference plugins; note
bin_diris deprecated in favour ofbin_dirsfrom containerd 2.1 - cgroup: cgroup v2 with the systemd cgroup driver (see our Kubernetes cgroup v2 migration guide)
- Tooling: a
crictlbuild that matches your Kubernetes minor version - Images: every image in use must be OCI or Docker Schema 2
- Access: the ability to cordon and drain nodes within your PodDisruptionBudgets
Step-by-Step containerd 2.0 Migration
This containerd 2.0 migration workflow applies to kubeadm, k3s, RKE2 and self-managed cloud node groups.
Step 1: Inventory runtime versions across the cluster
Start with the API server, which already knows every node’s runtime:
kubectl get nodes -o custom-columns=\
NAME:.metadata.name,\
KUBELET:.status.nodeInfo.kubeletVersion,\
RUNTIME:.status.nodeInfo.containerRuntimeVersion,\
OS:.status.nodeInfo.osImage
Any node that shows containerd://1.6.x or containerd://1.7.x goes on the migration list. If you scrape kubelet metrics, alert on kubelet_cri_losing_support as well.
Step 2: Find Docker Schema 1 images before they fail
List every unique image currently scheduled in the cluster:
kubectl get pods -A -o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{range .spec.initContainers[*]}{.image}{"\n"}{end}{end}' \
| sort -u > images.txt
wc -l images.txt
Then check each manifest’s schema version. This short Python script uses crane (from go-containerregistry) and flags Schema 1 images:
#!/usr/bin/env python3
"""schema1_scan.py - flag Docker Schema 1 images that containerd 2.x refuses to pull."""
import json
import subprocess
import sys
bad = []
for image in open(sys.argv[1]).read().split():
try:
raw = subprocess.run(["crane", "manifest", image],
capture_output=True, text=True, check=True).stdout
manifest = json.loads(raw)
except Exception as exc: # auth errors, deleted tags, etc.
print(f"SKIP {image}: {exc}")
continue
if manifest.get("schemaVersion") == 1:
bad.append(image)
print(f"SCHEMA1 {image}")
print(f"\n{len(bad)} Schema 1 image(s) found")
sys.exit(1 if bad else 0)
Run it with python3 schema1_scan.py images.txt. Rebuild or re-tag every flagged image. Re-pushing an old image with a modern Docker or Buildah client is usually enough to convert it to Schema 2.
Step 3: Find workloads that talk to the containerd socket
Agents that mount containerd.sock may still use CRI v1alpha2. Find them:
kubectl get pods -A -o json | jq -r '
.items[] | select(any(.spec.volumes[]?; .hostPath.path? // "" | test("containerd"))) |
"\(.metadata.namespace)/\(.metadata.name)"'
Typical hits include log shippers, image scanners, runtime-security sensors and CI runners. Check each vendor’s release notes for containerd 2.x support and upgrade them before the runtime.
Step 4: Convert config.toml to version 3
containerd 2.x still reads version 2 configs and converts them on every start. That works, but it slows startup and hides deprecated settings. Convert once and commit the result:
sudo containerd config migrate > /tmp/config-v3.toml
diff -u /etc/containerd/config.toml /tmp/config-v3.toml | less
A clean, minimal version 3 configuration for Kubernetes nodes looks like this:
# /etc/containerd/config.toml - containerd 2.x
version = 3
[plugins.'io.containerd.cri.v1.images']
snapshotter = 'overlayfs'
[plugins.'io.containerd.cri.v1.images'.pinned_images]
sandbox = 'registry.k8s.io/pause:3.10.2'
[plugins.'io.containerd.cri.v1.images'.registry]
config_path = '/etc/containerd/certs.d'
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc]
runtime_type = 'io.containerd.runc.v2'
[plugins.'io.containerd.cri.v1.runtime'.containerd.runtimes.runc.options]
SystemdCgroup = true
[plugins.'io.containerd.cri.v1.runtime'.cni]
bin_dirs = ['/opt/cni/bin']
conf_dir = '/etc/cni/net.d'
Note the three structural changes from version 2:
- The old
io.containerd.grpc.v1.criplugin is split intoio.containerd.cri.v1.imagesandio.containerd.cri.v1.runtime. - The pause image moves from
sandbox_imagetopinned_images.sandbox. - Registry settings shrink to a single
config_path.
Match the pause image tag to the one your Kubernetes version expects (kubeadm config images list shows it).
Step 5: Move registry mirrors to hosts.toml
Each registry gets its own directory under config_path. For a Docker Hub pull-through mirror:
# /etc/containerd/certs.d/docker.io/hosts.toml
server = "https://registry-1.docker.io"
[host."https://mirror.registry.internal"]
capabilities = ["pull", "resolve"]
For a private registry with an internal CA:
# /etc/containerd/certs.d/registry.corp.example:5000/hosts.toml
server = "https://registry.corp.example:5000"
[host."https://registry.corp.example:5000"]
capabilities = ["pull", "resolve", "push"]
ca = "/etc/containerd/certs.d/registry.corp.example:5000/ca.crt"
Changes to hosts.toml are picked up without restarting containerd, which makes mirror changes far safer than editing config.toml.
Move any static auths credentials into Kubernetes Secrets and reference them with imagePullSecrets or a service account.
Step 6: Upgrade one canary node
Drain the node, install the new runtime, apply the config and validate:
NODE=worker-01
kubectl cordon "$NODE"
kubectl drain "$NODE" --ignore-daemonsets --delete-emptydir-data --timeout=10m
# On the node
sudo systemctl stop kubelet containerd
sudo install -m 0644 /tmp/config-v3.toml /etc/containerd/config.toml
# install containerd 2.3.x + runc 1.2+ from your distro repo or release tarballs
sudo systemctl daemon-reload
sudo systemctl start containerd kubelet
containerd --version
sudo crictl info | jq '.config.containerd.runtimes.runc.options.SystemdCgroup'
sudo crictl pull registry.k8s.io/pause:3.10.2
# Back on your workstation
kubectl uncordon "$NODE"
kubectl get node "$NODE" -o wide
Schedule a test Deployment onto the canary with a node selector, then leave it running through at least one normal production deploy.
Step 7: Roll the rest of the fleet with Ansible
Once the canary is healthy, automate the rollout in small batches. serial keeps the blast radius under control:
# containerd2-rollout.yml
- name: Roll containerd 2.x across worker nodes
hosts: k8s_workers
serial: "20%"
become: true
tasks:
- name: Drain node
ansible.builtin.command: >
kubectl drain {{ inventory_hostname }} --ignore-daemonsets
--delete-emptydir-data --timeout=600s
delegate_to: localhost
become: false
- name: Install containerd 2.3
ansible.builtin.package:
name: containerd
state: latest
- name: Deploy version 3 config
ansible.builtin.copy:
src: files/config-v3.toml
dest: /etc/containerd/config.toml
mode: "0644"
- name: Restart runtime and kubelet
ansible.builtin.systemd:
name: "{{ item }}"
state: restarted
daemon_reload: true
loop: [containerd, kubelet]
- name: Wait for node Ready
ansible.builtin.command: >
kubectl wait --for=condition=Ready node/{{ inventory_hostname }} --timeout=300s
delegate_to: localhost
become: false
- name: Uncordon node
ansible.builtin.command: kubectl uncordon {{ inventory_hostname }}
delegate_to: localhost
become: false
On cloud platforms, prefer replacing nodes with a new image over in-place package upgrades. Immutable node pools make rollback a matter of scaling the old pool back up.
Step 8: Upgrade the control plane last
Only when the containerd 2.0 migration is complete and every node reports containerd://2.x should you upgrade Kubernetes past 1.35. Re-run the Step 1 command as a gate in your upgrade pipeline.
Architecture & Component Breakdown
Where containerd sits in the node stack
- The kubelet calls the CRI v1 gRPC API over
/run/containerd/containerd.sock. - The CRI images service (
io.containerd.cri.v1.images) resolves registries viahosts.toml, pulls layers and unpacks them with the snapshotter. - The CRI runtime service (
io.containerd.cri.v1.runtime) creates the pod sandbox through the Sandbox API and sets up networking via CNI. - The shim (
containerd-shim-runc-v2) starts and supervises each container through runc. - Optional NRI plugins can adjust container specs, and CDI injects devices such as GPUs.
Version 2 vs version 3 configuration
| Setting | Config version 2 (containerd 1.x) | Config version 3 (containerd 2.x) |
|---|---|---|
| CRI plugin ID | io.containerd.grpc.v1.cri | io.containerd.cri.v1.images + io.containerd.cri.v1.runtime |
| Pause image | sandbox_image | pinned_images.sandbox |
| Registry mirrors | registry.mirrors | registry.config_path + hosts.toml |
| Registry auth | registry.configs.auth | imagePullSecrets |
| CNI binaries | cni.bin_dir | cni.bin_dirs (list) |
| Runtime shim | runc.v1 or runc.v2 | io.containerd.runc.v2 only |

What containerd 2.x gives you in return
- Transfer service (stable) for consistent image pull and push semantics
- Image verifier plugins that can block unsigned images at pull time
- CRI user namespaces (with runc 1.2+) for stronger pod isolation
- Recursive read-only mounts for safer volume handling
- Deprecation warnings exposed through the Introspection API, visible with
ctr deprecations list
Real-World Use Cases & Enterprise Examples
Scenario 1: kubeadm cluster with a private registry mirror
An on-premises platform team runs 40 kubeadm workers with a Harbor pull-through cache configured under registry.mirrors. They build a new golden image with containerd 2.3, convert the config with containerd config migrate, and template hosts.toml files per registry in Ansible.
The containerd 2.0 migration canary exposes one surprise: a legacy batch job image built in 2016 is Schema 1. Rebuilding it takes ten minutes. Finding it in production would have taken an outage.
Scenario 2: CI runners that use the containerd socket
A company runs GitLab or Jenkins build agents that mount containerd.sock and drive builds with an old nerdctl. After the upgrade, builds fail with CRI API errors.
The fix is to upgrade the build tooling first, or better, move builds to rootless BuildKit in its own pod so CI never depends on the node runtime.
Scenario 3: GKE cluster with paused auto-upgrades
A team notices their GKE cluster has stayed on 1.32 for weeks. GKE detected DEPRECATION_CONTAINERD_V1_SCHEMA_IMAGES usage and paused automatic upgrades. They use the insight to find the image, replace it, and auto-upgrades resume after the detection window passes.
Scenario 4: GPU nodes and the NVIDIA Container Toolkit
GPU pools often carry custom runtime entries such as nvidia under the runtimes table. Re-generate that entry for config version 3 with a current NVIDIA Container Toolkit, or switch to CDI, which containerd 2.x enables by default.
Troubleshooting, Edge Cases & Best Practices
Common errors and fixes
Image pull fails with a schema 1 error
The image uses Docker Schema 1. Rebuild and re-push it. As a temporary measure only, set the environment variable in a systemd drop-in:
sudo mkdir -p /etc/systemd/system/containerd.service.d
cat <<'EOF' | sudo tee /etc/systemd/system/containerd.service.d/schema1.conf
[Service]
Environment=CONTAINERD_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE=1
EOF
sudo systemctl daemon-reload && sudo systemctl restart containerd
Remove the drop-in as soon as the image is replaced.
“unknown service runtime.v1alpha2.RuntimeService”
A client is still using CRI v1alpha2. Upgrade crictl, the monitoring agent or the security sensor that produced the error.
Kubelet reports the node NotReady after restart
Check journalctl -u containerd -b for config parse errors. Mixing version 2 table names inside a version = 3 file is the most common cause.
Mirror is ignored and pulls go straight to Docker Hub
config_path is not set, or the directory name does not match the registry host exactly. For Docker Hub the directory must be docker.io.
Pods suddenly bind to ports below 1024
That is the new unprivileged-ports default. Disable it in the CRI runtime section if your policy requires it, and enforce the rule with Pod Security admission.
Apps using io_uring fail with permission errors
The default seccomp profile now blocks io_uring. Grant it with a custom seccomp profile only for workloads that need it.
containerd 2.0 migration rollback plan
Every containerd 2.0 migration needs a tested way back. Write it down before the first canary:
- Keep the old node image (or package version) published and tagged for at least two weeks.
- Back up
/etc/containerd/on in-place upgrades, including the oldconfig.toml. - Expect to re-pull images. Snapshots created by containerd 2.x are not guaranteed to be reused cleanly after a downgrade, so plan bandwidth or a warm registry cache.
- Roll back by replacement, not by editing. Cordon the bad node, scale up the old pool, and drain workloads back.
A rollback should be a five-minute operation that anyone on call can run, not a late-night debugging session.
containerd 2.0 migration best practices
- Target containerd 2.3 LTS to avoid another runtime migration in 2027.
- Upgrade runtime and cgroup v2 together. Both are required past Kubernetes 1.35, so rebuild node images once.
- Commit the version 3 config to Git. Do not rely on automatic conversion at every boot.
- Run
ctr deprecations liston every canary node and fix each warning. - Scan images in CI for Schema 1 manifests so new ones never reach production.
- Upgrade socket-mounting agents first, then the runtime.
- Keep one old node pool until the new pool has survived a real production deploy.
FAQ
Does Kubernetes 1.36 support containerd 1.7?
No. Kubernetes 1.35 is the last minor release that supports containerd 1.x. Upstream containerd 1.7 extended support also ended on 30 September 2026, so nodes must move to containerd 2.x before the cluster is upgraded past 1.35.
How do I convert my containerd config.toml to version 3?
Run containerd config migrate on a node with containerd 2.x installed and redirect the output to a new file. Review the diff, then commit it. containerd 2.x can convert version 2 configs automatically at startup, but a committed version 3 file starts faster and exposes deprecated settings.
Why does my image fail to pull after upgrading to containerd 2.0?
The image most likely uses the Docker Schema 1 manifest format, which containerd 2.0 disables by default. Rebuild and re-push the image as Docker Schema 2 or OCI. The CONTAINERD_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE=1 variable is only a temporary workaround.
Where do registry mirrors go in containerd 2.x?
Set config_path under the io.containerd.cri.v1.images registry section, usually to /etc/containerd/certs.d. Then create one directory per registry host containing a hosts.toml file with the mirror endpoints. Static credentials should move to Kubernetes imagePullSecrets.
Which containerd 2.x version should I upgrade to?
For most production clusters, containerd 2.3 is the best choice because it is a long-term support release maintained until April 2028. containerd 2.0 LTS is supported only until March 2027, and 2.2 support ends in November 2026.
Summary & Key Takeaways
- Kubernetes 1.35 is the last release that supports containerd 1.x, and containerd 1.7 extended support ended on 30 September 2026.
- containerd 2.3 LTS (supported to April 2028) is the best migration target for most fleets.
- Audit first: runtime versions, Docker Schema 1 images and workloads that mount
containerd.sock. - Convert to config version 3 with
containerd config migrateand commit the result. - Move registry mirrors to
hosts.tomlunderconfig_path, and credentials toimagePullSecrets. - Watch the behaviour changes: NRI, CDI and unprivileged ports are on by default, and
io_uringis blocked. - Roll nodes in small batches and upgrade the control plane only after every node runs containerd 2.x.
A containerd 2.0 migration is mostly an audit exercise. Find the Schema 1 images, the v1alpha2 clients and the old registry config before you upgrade, and the runtime swap itself becomes a routine node rotation.
For the other half of your node readiness work, read the Kubernetes cgroup v2 migration guide. If your clusters still use Ingress NGINX, plan the Gateway API migration in the same maintenance cycle.
Editorial note: Removed and deprecated features come from the official containerd 2.0 documentation and the containerd CRI configuration reference. Support dates follow endoflife.date as of October 2026. GKE behaviour follows Google’s Migrate nodes to containerd 2 guide. Test every change on a canary node before production.
