Skip to main content
Logo
Overview

containerd 1.7 End of Support (Sep 1, 2026): Migrating Kubernetes Nodes to containerd 2.x

July 2, 2026
10 min read

There’s a date on the calendar that most people running Kubernetes haven’t circled yet: September 1, 2026. That’s when security-only support for the containerd 1.7 branch stops. After that, no CVE backports, no patches — the runtime under every pod on your nodes goes unmaintained.

If you run managed Kubernetes, your provider will handle a lot of this for you, and you may already have gotten a nudge in the console. If you run your own nodes — kubeadm, kops, bare metal, self-baked AMIs — this one’s on you. And the jump from 1.7 to 2.x is not a rubber-stamp apt upgrade. There are removed APIs, renamed config keys, and a registry config format that changed twice. Miss one of them and your kubelet can’t talk to the runtime, or your nodes quietly stop pulling images from your private registry.

Here’s what’s actually happening, what breaks, and the order to do things in so you’re not debugging a CreateContainerError at 2am.

The deadline, precisely

The containerd committers stopped supporting the 1.7 release branch on March 10, 2026. That would normally be the end of it. But because so much of the managed-Kubernetes world still runs 1.7, two maintainers (Samuel Karp and Chris Henzie) picked up an extended, security-only window that runs through September 2026.

That extension is narrow. It’s scoped to containerd 1.7 as used with Kubernetes 1.30, 1.31, and 1.32 on Google Kubernetes Engine. It is not a general “1.7 is fine until September” promise for every distro and cloud. If you’re on 1.7 outside that lane, you’re arguably already past support today. Treat September 1 as the hard wall and plan to be off 1.7 well before it — not on August 31.

The 2.0 branch, for what it’s worth, is on a similar clock: committer support ended November 7, 2025, with extended security support through March 2027 (scoped to Kubernetes 1.33 on GKE). So “just go to 2.0” buys you less runway than you’d think. More on version choice in a second.

Which 2.x version to land on

containerd 2.x has two LTS lines in active support right now:

  • 2.0 LTS — supported until March 1, 2027.
  • 2.3.x LTS — the current LTS, supported until April 30, 2028. (The 2.3.1 patch shipped in June 2026.)

The upgrade rules matter here. containerd guarantees a tested, supported direct upgrade between sequential LTS releases. A direct 1.7 → 2.3 jump is tested and supported. A jump like 1.7 → 2.6 (a future LTS) is explicitly not — you’d have to hop through an intermediate.

So the call is easy: go to 2.3, not 2.0. Same migration effort, and you get almost two extra years of patches (April 2028 vs March 2027). The only reason to target 2.0 is if your Kubernetes distro or vendor image pins it, and even then I’d push to get onto the newer line as soon as the vendor supports it.

One caveat worth flagging before you upgrade: the config migration logic that makes this smooth was written to move you from 1.6/1.7 to 2.0. Going straight to 2.3 works, but 2.1 and 2.2 each added their own deprecations on top of 2.0’s. Read the section on config below — some of the sharpest edges landed after 2.0.

What actually breaks in 2.x

This is the part people underestimate. containerd 2.0 was a major version for a reason — it dropped a pile of long-deprecated surface area. The ones that bite in practice:

CRI v1alpha2 is gone. This is the big one for Kubernetes. The old CRI v1alpha2 API was deprecated in 1.7 and removed in 2.0. Anything still calling it — an old kubelet, a node agent, a monitoring sidecar that talks CRI directly — gets hard failures, not warnings. Kubernetes itself moved to CRI v1 back in the 1.26 era, so a reasonably current kubelet is fine. But if you’re dragging along a very old Kubernetes version and trying to jump the runtime, sort the kubelet first.

AUFS snapshotter is removed. If you somehow still have nodes on AUFS, switch to overlayfs (or btrfs) before the upgrade. Most modern kernels default to overlayfs anyway, so this mostly hits ancient hosts.

Docker Schema 1 images won’t pull. Support for the legacy Schema 1 image format is gone. If any workload still references an image published in that format — usually very old vendor images or something built on a decade-old registry — the pull fails on 2.x. This is a sneaky one because it doesn’t show up until that specific pod tries to schedule. Audit your image references ahead of time; anything you haven’t rebuilt since roughly 2017 is suspect.

The registry config format changed — twice. This is where I’ve seen the most self-inflicted outages, so it gets its own section.

The registry config trap

If your nodes pull from a private registry or a pull-through mirror, read this carefully.

The old inline style — [plugins."io.containerd.grpc.v1.cri".registry.mirrors] and friends in config.toml — has been deprecated since containerd 1.5. The replacement is config_path: you point containerd at a directory of hosts.toml files, one per registry host. In 2.x the old registry.configs, registry.auths, and registry.mirrors keys are on the way out (and registry.configs.tls was already removed in 2.0).

The trap is the automatic migration. When containerd 2.x reads an old v2 config.toml that still has inline registry.mirrors, the migrator can produce a config that has both config_path and mirrors set. containerd 2.2 treats that as a conflict and the CRI plugin refuses to load — you get a node where the kubelet comes up but can’t create any containers, because the runtime plugin errored out. The error is explicit (“mirrors cannot be set when config_path is provided”), but you only see it if you’re looking at the containerd logs, not the kubelet’s.

The fix is to not rely on the auto-migrator for registry config. Before upgrading, convert your mirrors to the hosts.toml / config_path model by hand on 1.7, confirm pulls still work, then upgrade. That way there’s no inline registry.mirrors left for the migrator to trip over.

While you’re in there, note the bin_dirbin_dirs change too: the single-value CNI bin_dir under the CRI runtime plugin was deprecated in 2.1 (it’s now an array, bin_dirs) and removed in 2.2. Small, but it’ll throw a deprecation warning that’s easy to miss in the noise.

Do the boring pre-flight first

containerd built a genuinely nice on-ramp for this, and almost nobody uses it. The latest 1.6 and 1.7 releases emit deprecation warnings for any config that won’t survive the jump to 2.0. That’s your migration checklist, generated for you, specific to your actual config.

Check what your running daemon is complaining about:

# Warnings show up as a "deprecations" list in the daemon info
ctr version
crictl info | jq '.config.containerd'
sudo journalctl -u containerd | grep -i deprecat

If containerd reports zero deprecation warnings on 1.7, your upgrade to 2.x should be smooth — that’s the whole point of the warning system. If it reports something, fix it on 1.7, restart, confirm the warning is gone, and only then plan the version bump. Fixing config on the version you already run is reversible and low-drama. Fixing it while also swapping the binary is how you end up bisecting two changes at once.

Your pre-flight, in order:

  1. Turn up the deprecation warnings on every node pool / node class and collect them.
  2. Convert registry mirrors to config_path + hosts.toml, verify pulls.
  3. Confirm your kubelet / Kubernetes version speaks CRI v1 (anything 1.26+ is fine).
  4. Audit image references for Schema 1 stragglers.
  5. Confirm snapshotter is overlayfs, not AUFS.

Clear all five and the actual upgrade is almost anticlimactic. That’s the goal.

Migrating by environment

Self-managed nodes (kubeadm, kops, bare metal). Don’t upgrade containerd in place on a live node if you can avoid it. The clean pattern is drain-and-replace: cordon the node, kubectl drain, replace it with a node built on a 2.3 image (new AMI, new cloud-init, whatever your provisioning is), uncordon. This gives you a trivial rollback — if the new node misbehaves, you delete it and the old one never left the fleet until you were happy. In-place apt/yum upgrades work, but you’re mutating a running node and rollback means reinstalling the old package under load. Replace beats mutate here.

Roll it out one node pool at a time, watch pods reschedule cleanly, and keep an eye on crictl health on the first replaced node before you touch the rest.

GKE. Google is driving this migration on its own timeline and will move you if you don’t. GKE pauses automatic upgrades to 1.33 for Linux node pools when it detects deprecated feature usage, and resumes once it sees 14 days with no such usage (3 days if the only issue is deprecated CRI registry.configs properties). Translation: GKE is watching your nodes for the exact deprecations above and holding your cluster back until they’re gone. If you don’t migrate, when 1.32 hits end of support and nodes auto-upgrade to 1.33, workloads on Docker Schema 1 images can fail. Google publishes a find-nodepools-to-migrate.sh helper in its k8s-node-tools repo — run it to see which pools still need attention. The fix is the same config cleanup; GKE just enforces the deadline for you.

EKS / AKS. Both bundle containerd in their managed AMIs/node images and bump it as you move Kubernetes versions, so the runtime upgrade rides along with your normal node-group version upgrades. Your job is the same pre-flight — deprecation warnings, registry config, image audit — plus staying on a supported Kubernetes version. If you’re already thinking about node OS lifecycles, this pairs with the Amazon Linux 2 end-of-life migration work, since AL2023 nodes ship newer containerd anyway.

Verifying you actually landed it

Once a node’s on 2.3, don’t trust “the node is Ready.” Ready means the kubelet is happy; it doesn’t prove the runtime is fully wired. Smoke-test the runtime directly:

# Runtime is 2.x and CRI is responding
crictl version
crictl info | jq '.config.sandboxImage, .config.containerd.runtimes'
 
# A real pull through your registry path (not just docker.io)
crictl pull your-registry.example.com/some/image:tag
 
# Existing pods are actually running, not stuck in CreateContainerError
crictl ps -a | grep -iv running

The three things I’d specifically confirm: image pulls from your private registry succeed (that’s the registry-config change paying off or biting), your custom runtime handlers still resolve if you use gVisor/Kata (runtimes in the info output), and no pod is stuck in a container-create loop. If all three are clean on the first replaced node, the rest of the fleet will follow.

Rollback plan, since you should always have one: with drain-and-replace, rollback is deleting the new node and letting the old-image pool absorb the pods. Keep one 1.7 node pool alive and cordoned-but-present until you’ve run a full day of production traffic on 2.3. Cheap insurance.

Where this sits in the bigger cleanup

This isn’t happening in isolation. containerd 2.x, the Kubernetes 1.35 cgroup v1 removal, and the general node-OS churn are all landing in the same rough window, and they touch the same nodes. If you’re going to drain-and-replace your fleet anyway, it’s worth sequencing these so you’re not rebuilding nodes three separate times. And if the runtime upgrade has you re-examining node costs while you’re in there, the EKS pricing breakdown is a decent companion — extended-support surcharges have a way of showing up in exactly these lagging-version situations.

The lazy-but-correct move: turn on deprecation warnings this week, before you plan anything else. The output tells you whether you have a five-minute config fix or a real project — and it’s the one step that makes every other step safe.