containerd 1.7 End of Life Migration: The Deadline That Already Landed

Kubernetes dropped containerd 1.x support in v1.36.0 and containerd 1.7 goes end of life this month. The deadline most upgrade guides quote is the wrong one.

By VVV Ops ·

Your upgrade board probably has a containerd line item parked under "Kubernetes 1.37, sometime next year". Move it. The containerd 1.7 end of life migration is not a 1.37 problem and never was. Kubernetes dropped containerd 1.x support in v1.36.0, and containerd 1.7 itself reaches end of life in September 2026. The deadline most upgrade guides are quoting is a different deadline, attached to different code, and it has now slipped twice. We keep meeting teams who built a careful plan around the wrong date and still have 1.7 running on every node.

Two deadlines, and only one of them moved

The date being passed around comes from the kubelet's deprecated configuration flags and the CRI fallback behavior wired to them. That removal was scheduled for 1.36, pushed to 1.37, then pushed again. The 1.37 changelog is unambiguous about it:

kubelet: Deferred the deprecation removal timeline for the configuration flags (and the related fallback behavior) from v1.37 to v1.38 to align with containerd v1.7 support.

That was PR #139121, merged in May 2026. The 1.36 changelog carries the earlier slip, PR #136846, which moved the same removal from 1.36 to 1.37. Two deferrals on the same line of work. If your plan cites 1.37 for anything containerd-related, it was probably written against a changelog that has since been amended.

The deadline that matters did not move, and it is older than either deferral. SIG Node settled it back in the 1.34 cycle, in the cgroup driver autoconfiguration announcement:

The last Kubernetes release to offer this support will be the last released version of v1.35, and support will be dropped in v1.36.0.

Be precise about what "dropped" means, because the plans we see get it wrong in both directions. The kubelet now asks the container runtime which cgroup driver to use instead of being told by a flag. That lookup went GA in 1.34, and per the same announcement containerd only answers it from v2.0.0 onward, while CRI-O has answered it since v1.28.0. A containerd 1.7 node does not answer. What happens next is not a crash. The kubelet logs CRI implementation should be updated to support RuntimeConfig. Falling back to using cgroupDriver from kubelet config. and carries on with the flag. That fallback is exactly the code the two deferrals kept alive, and it is now due to go in 1.38. So a 1.36 or 1.37 kubelet on containerd 1.7 still boots. It is also untested upstream, refused by EKS (more on that below), and about to sit on a runtime that gets no more patches. "Still boots" is the reason so many fleets have not noticed they are out of support.

So the real sequence is: containerd 1.x support ended with Kubernetes 1.36.0, which shipped in April 2026, and containerd 1.7 goes end of life in September 2026 under the project's own release policy. If you are still on 1.7 this month, you are not planning a migration. You are late, and the runway you think you have is the runway for a different change.

Find the nodes that are out of support, today

Before anyone argues about sequencing, get the inventory. Kubernetes already reports the runtime version on every node object, so this takes one command and no new tooling:

# Every node, its runtime, and the version. Anything starting
# containerd://1. is unsupported on 1.36 and blocks an EKS upgrade.
kubectl get nodes -o json \
  | jq -r '.items[] | [.metadata.name, .status.nodeInfo.containerRuntimeVersion] | @tsv' \
  | sort -k2

The containerRuntimeVersion field prints values like containerd://1.7.27 or containerd://2.3.1. Sort on the second column and the problem nodes group themselves.

If you run Prometheus against your kubelets, SIG Node added a purpose-built signal for exactly this transition. From the same announcement:

You are able to monitor the kubelet_cri_losing_support metric to determine if any nodes in your cluster are using a containerd version that will soon be outdated. The presence of this metric with a version label of 1.36.0 will indicate that the node's containerd runtime is not new enough for the upcoming requirements.

One correction to that quote, taken from the kubelet source rather than the docs. The version label names the release in which the fallback is due to be removed, and it has moved with every deferral. Current 1.34, 1.35 and 1.37 kubelets label it 1.38.0, and the 1.36 branch still says 1.37.0. A query pinned to 1.36.0 matches nothing on a patched fleet. Do not filter on the label. Alert on the metric being present at all:

# Counter, incremented once at kubelet start on any node whose runtime
# does not answer RuntimeConfig. Any series at all is a node to fix.
kubelet_cri_losing_support > 0

Put that on the platform dashboard and alert on it being non-empty. It is a better gate than a spreadsheet, because it keeps working after the migration when someone reintroduces an old node image through a Terraform module nobody re-reviewed. We would wire this up even if the migration were already finished.

Which containerd 2.x line to land on

"Upgrade to containerd 2" is not an instruction. There are six branches with wildly different lifespans, and two of them are already dead. Here is the project's own support table:

| containerd line | Status | End of life | Our call | |---|---|---|---| | 1.7 | LTS | September 2026 | Leaving, this month | | 2.0 | LTS | March 2027 | Acceptable only if your distro pins it | | 2.1 | End of Life | July 3, 2026 | Already unsupported, do not target | | 2.2 | Active | November 6, 2026 | Buys you weeks, not quarters | | 2.3 | LTS | April 30, 2028 | Target this | | 2.4 | Active | May 16, 2027 | Only for a feature you actually need |

Land on 2.3. It is the LTS line with the longest runway, and it is the only choice that does not put another runtime migration on the board inside eighteen months. The failure mode we see most often is a team that reads "containerd 2.0 or later", takes whatever 2.x their base image offers, and lands on 2.1, which reached end of life on July 3, 2026. That is a migration completed into an unsupported branch, which is worse than not starting, because the urgency is now gone and the exposure is not.

EKS shows how it happens. Amazon's own docs note that EKS "Containerd updated to 2.1 in Version 1.34 for launch". Anyone who upgraded to EKS 1.34 early and pinned their AMI is sitting on an end-of-life runtime right now. AWS has since moved forward, with AMI release v20260709 shipping containerd 2.2.4-1.amzn2023.0.3, but a pinned AMI does not move on its own.

Your node image picks the version, not you

On managed control planes, the containerd version is a property of the node image, which means most of this migration is an AMI or node-pool decision rather than a package upgrade.

On GKE, Google has already done the work on your behalf for recent versions. Per the containerd 2 migration guide, "Linux nodes that run GKE 1.33 use containerd 2.0" and "Windows Server nodes that run GKE 1.35 use containerd 2.0". GKE also pauses upgrades rather than breaking you, holding until the end of standard support for 1.32 on Linux and 1.34 on Windows Server. Your job there is the configuration that rides along with the runtime, covered in the next section.

On EKS, you choose the AMI, so you own the version. AWS states the rule plainly in the 1.35 release notes: "Kubernetes 1.35 is the last release supporting containerd 1.x. You must switch to containerd 2.0 or later before upgrading to the next Kubernetes version." If you build custom AMIs, this is a real project with a testing cycle. If you use managed node groups on current EKS-optimized AMIs, it is mostly picking a version and soaking it.

Two EKS details that catch people. Bottlerocket sets failCgroupV1: false in its kubelet configuration, which preserves backward compatibility and quietly hides a cgroup problem you would otherwise have found in 1.35. And Fargate continues to use cgroup v1, so Fargate profiles do not behave like your EC2 node groups here. Plan them separately.

The config file is the actual migration

Swapping the binary is the easy half. The configuration format changed in containerd 2.0, and this is where node pools come back unhealthy.

containerd moved to config version 3. Version 2 files still load and are converted automatically, but several plugin IDs were renamed, so the file you have is not the file you want to keep. There is a subcommand for this, and its help text carries a caveat worth reading twice:

# Inspect what containerd actually resolved, imports included
containerd config dump > /tmp/containerd-effective.toml

# Rewrite the config file to the current version.
# Note: this does NOT migrate subconfig files pulled in via `imports`.
containerd config migrate

If you use imports to compose configuration from a drop-in directory, and most teams running a config-managed fleet do, containerd config migrate will leave those fragments untouched. You have to walk them yourself.

The registry configuration is the sharpest edge. Per the CRI plugin config guide, registry.configs and registry.mirrors from containerd 1.4 "are now DEPRECATED and will only be used if the config_path is not specified. It is an error to specify both config_path and the deprecated configs or mirrors." Google's migration guide goes further and warns that registry.auths, registry.configs and registry.mirrors will be removed in containerd 2.4 or later, with registry.configs.tls already gone in 2.0.

Those two rules collided in a real regression. In containerd 2.2.0, migrating a version 2 config that used registry.mirrors produced a version 3 config containing both a config_path and a mirrors block, and the plugin refused to load with:

unable to load CRI image service plugin dependency: invalid cri image config: `mirrors` cannot be set when `config_path` is provided

That is issue #12612, closed by PR #12617. It is fixed, but it tells you where the landmines are. Do not carry registry.mirrors across this migration hoping the converter handles it. Move your registry configuration to the certs.d layout under config_path as a deliberate step, verify it with containerd config dump on one node, and only then roll the fleet.

What else breaks on the way across

The runtime is one of several node-level changes landing in the same upgrade window, and they interact. From the EKS release notes and the upstream changelogs:

  • Kubernetes 1.35 deprecates cgroup v1, and the kubelet refuses to start by default on a cgroup v1 node. The failCgroupV1: false setting exists, but treat it as a stay of execution, not a fix.
  • Manual --cgroup-driver configuration was deprecated in 1.34 in favor of the autodetection described above. Strip it from bootstrap scripts and custom AMI configuration now, because it is the flag class the 1.38 removal is aimed at.
  • The --pod-infra-container-image flag was removed from the kubelet in 1.35. Custom AMI users have to remove it before upgrading.
  • In 1.37 the deprecated cAdvisor flags stop being accepted entirely. Per the changelog, "the kubelet will fail to start if any are set (only --housekeeping-interval is kept)", including --containerd and --containerd-namespace. Those two names look like they belong in a containerd migration checklist. They do not, and setting them will take a node down.

Audit the kubelet command line at the same time you audit the containerd config. They ship on the same node image and they fail the same way, which is a node that never becomes Ready.

The order we run it in

Containerd first, kubelet second, or both together in one image. Never the kubelet first. SIG Node's wording is the rule: upgrade containerd to v2.0 or later "before, or at the same time as, upgrading the kubelet to v1.36.0". On managed node groups both arrive together in the AMI, which is fine and is the outcome you want anyway.

For the rollout mechanics, one node pool, a real soak, then the fleet. We wrote up the node-by-node cutover pattern and the managed-cluster constraints in detail for the kube-proxy IPVS to nftables migration, and the same sequencing applies here without modification. If you are planning your 1.36 hop as a single project, migrating mutating webhooks to admission policies covers the control-plane half of the same release.

One scheduling note. Do this before you need it, not during a security patch. A containerd CVE that requires a fleet-wide runtime bump is a bad moment to discover your registry mirrors do not survive the config conversion.

When to Get Help

Most teams can do this themselves. The ones that struggle share a profile: custom AMIs, a config-managed containerd with imports fragments nobody has read in two years, private registry mirrors, and a 1.34 or 1.35 control plane with an upgrade already promised to the business.

If that is your cluster and you want the inventory, the version decision, and the config conversion done by someone who has run it before, talk to us. Bring the output of the kubectl get nodes command above and your current /etc/containerd/config.toml. Those two artifacts tell us within a day whether this is a two-week change or a quarter.

Tags: containerd 1.7 end of life migration, kubernetes automation, kubernetes consulting, infrastructure as code, devops automation services, container orchestration