How to Upgrade a kubeadm Cluster to Kubernetes 1.37: Step by Step (2026)
This guide is for engineers who run self-managed Kubernetes clusters built with kubeadm and need to move them up one minor version without an outage. By the end you will have upgraded a cluster from 1.36.6 to 1.37.2 in the documented order: etcd backup first, a deprecated API scan, the package repository switch, the first control plane node, the remaining control plane nodes, then every worker, with a rollback path at each step. Commands match the official Upgrading kubeadm clusters task as it reads this week, with packages from pkgs.k8s.io (kubeadm, kubelet and kubectl 1.37.2-1.1), Pluto 5.24.5 and etcdctl 3.6.8. You need SSH and sudo on every node, a kubeconfig with cluster-admin, and ideally a staging cluster on the same version to rehearse on. If you want to know what changed in 1.37 before you start, read What's new in Kubernetes 1.37 and 1.38 first.
Step 1: Check current versions and the skew rules
Start by recording where you are. The version skew policy is the rulebook here: the control plane moves one minor version at a time, kube-apiserver instances in an HA cluster may differ by at most one minor version during the upgrade, and kubelets may be up to three minor versions older than the API server but never newer. kubeadm enforces the same thing in code: MaximumAllowedMinorVersionUpgradeSkew is 1 in the 1.37.2 upgrade policy.
# From a workstation with cluster-admin
kubectl version
kubectl get nodes -o custom-columns=NAME:.metadata.name,KUBELET:.status.nodeInfo.kubeletVersion,RUNTIME:.status.nodeInfo.containerRuntimeVersion
# On a control plane node
kubeadm version -o short
# v1.36.6
Expected output is every node on v1.36.x and every runtime on containerd://2.x. If any node shows containerd://1.7, stop and upgrade the runtime first: the 1.35 release notes named 1.35 as the last release supporting containerd 1.x, and kubeadm 1.37's preflight now warns when the runtime lacks the RuntimeConfig call. If you are on 1.35, do this whole guide twice, first to 1.36 and then to 1.37.
Step 2: Back up etcd and the kubeadm config before you touch anything
kubeadm writes its own etcd backup under /etc/kubernetes/tmp during upgrade apply, but that is a local copy on the node you are upgrading. Take a proper snapshot and copy it off the machine. Use an etcdctl that matches the etcd server, so check the image first; kubeadm 1.36.6 deploys etcd 3.6.8-0 by default per its constants.go.
# Which etcd is running?
kubectl -n kube-system get pods -l component=etcd -o jsonpath='{range .items[*]}{.spec.containers[0].image}{"\n"}{end}'
# registry.k8s.io/etcd:3.6.8-0
# On the first control plane node: install a matching etcdctl/etcdutl (v3.6.8)
ETCD_VER=v3.6.8
curl -fsSL -o /tmp/etcd-${ETCD_VER}.tgz \
https://github.com/etcd-io/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz
tar -xzf /tmp/etcd-${ETCD_VER}.tgz -C /tmp
sudo install -m 0755 /tmp/etcd-${ETCD_VER}-linux-amd64/etcdctl /tmp/etcd-${ETCD_VER}-linux-amd64/etcdutl /usr/local/bin/
# Snapshot the keyspace using the kubeadm-generated etcd certificates
sudo mkdir -p /var/backups/k8s
SNAP=/var/backups/k8s/etcd-$(date +%F-%H%M).db
sudo etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
snapshot save "$SNAP"
# Verify it, then archive the PKI, manifests and kubeconfigs
sudo etcdutl --write-out=table snapshot status "$SNAP"
sudo tar -czf /var/backups/k8s/etc-kubernetes-$(date +%F).tgz /etc/kubernetes
# Copy both files somewhere that is not this node
The snapshot command and certificate paths follow the Operating etcd clusters for Kubernetes task. Restores use etcdutl snapshot restore, which creates a new data directory; rehearse it on staging once so you are not reading the man page during an incident. If you also want application-level backups of namespaces and volumes, Velero covers that layer; an etcd snapshot is the cluster's brain, not your data.
Step 3: Scan for deprecated APIs and 1.37 breaking changes
The official deprecated API migration guide lists no GA API removals after 1.32, so a clean 1.36 cluster usually has nothing to rewrite. Scan anyway: Helm releases, CRDs and vendor charts still carry old apiVersions, and 1.37 dropped the alpha scheduling.k8s.io/v1alpha2 group outright. Pluto is actively maintained (5.24.5 shipped on 3 November 2026); kube-no-trouble (kubent) still works but its latest release, 0.7.3, dates from August 2024, so I would not rely on its rule set alone.
# Install Pluto 5.24.5
curl -fsSL -o /tmp/pluto.tgz \
https://github.com/FairwindsOps/pluto/releases/download/v5.24.5/pluto_5.24.5_linux_amd64.tar.gz
tar -xzf /tmp/pluto.tgz -C /tmp pluto
# Everything in the cluster and in Helm release manifests, against the target version.
# Exit code 2 = deprecated APIs found, 3 = removed APIs found.
/tmp/pluto detect-all-in-cluster -o wide --target-versions k8s=v1.37.0
/tmp/pluto detect-helm -o wide --target-versions k8s=v1.37.0
# What clients are actually calling, from the API server itself
kubectl get --raw /metrics | grep '^apiserver_requested_deprecated_apis'
Then check the 1.37 changes that are not API versions, all listed in the 1.37 changelog and release blog:
# cgroup v2 on every node ("cgroup2fs" is good, "tmpfs" means cgroup v1 and the kubelet will refuse to start)
stat -fc %T /sys/fs/cgroup/
# kube-proxy mode: ipvs is deprecated (default off by 1.40, removed by 1.43)
kubectl -n kube-system get configmap kube-proxy -o jsonpath='{.data.config\.conf}' | grep 'mode:'
# kubeadm v1beta3 config files are rejected by kubeadm 1.37; migrate any you keep in Git
grep -l 'kubeadm.k8s.io/v1beta3' /etc/kubernetes/*.yaml ~/kubeadm*.yaml 2>/dev/null
sudo kubeadm config migrate --old-config kubeadm-config.yaml --new-config kubeadm-config-v1beta4.yaml
Run kubeadm config migrate with the 1.36 binary, before you upgrade kubeadm. Also read the SELinuxMount note in the changelog if your nodes run SELinux in enforcing mode, since it is the one ACTION REQUIRED item most likely to break running Pods.
Step 4: Switch the pkgs.k8s.io repository to v1.37
The community repositories at pkgs.k8s.io are split per minor version, so apt on a node configured for v1.36 will never offer 1.37 packages. Change the repository on every node, as described in Changing the Kubernetes package repository.
# Debian / Ubuntu
grep pkgs.k8s.io /etc/apt/sources.list.d/kubernetes.list
# deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.36/deb/ /
sudo sed -i 's|/core:/stable:/v1.36/|/core:/stable:/v1.37/|' /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
apt-cache madison kubeadm | head -3
# kubeadm | 1.37.2-1.1 | https://pkgs.k8s.io/core:/stable:/v1.37/deb Packages
# RHEL / Rocky / Fedora: both baseurl and gpgkey contain the minor version
sudo sed -i 's|/core:/stable:/v1.36/|/core:/stable:/v1.37/|g' /etc/yum.repos.d/kubernetes.repo
# DNF4 (RHEL, Rocky and CentOS Stream, including EL10)
sudo dnf list --showduplicates kubeadm --disableexcludes=kubernetes
# DNF5 (Fedora 41+)
sudo dnf list --showduplicates kubeadm --setopt=disable_excludes=kubernetes
If madison shows nothing from pkgs.k8s.io, the node is still pointed at an old or mirrored repository; fix that before going further.
Step 5: Upgrade the first control plane node with kubeadm upgrade apply
Pick one control plane node that has /etc/kubernetes/admin.conf. Upgrade kubeadm only, look at the plan, and then apply.
sudo apt-mark unhold kubeadm && \
sudo apt-get update && sudo apt-get install -y kubeadm='1.37.2-1.1' && \
sudo apt-mark hold kubeadm
# RHEL: sudo dnf install -y kubeadm-'1.37.2-*' --disableexcludes=kubernetes
kubeadm version -o short
# v1.37.2
sudo kubeadm upgrade plan
upgrade plan checks that the cluster is upgradeable and prints a table of current and target versions for the API server, controller manager, scheduler, kube-proxy, CoreDNS and etcd. With kubeadm 1.37.2, expect CoreDNS v1.14.6 and etcd 3.7.0-0 as targets, per its constants.go. Read that table rather than trusting this paragraph; it is what your binary will actually do.
# Optional: see the manifest changes and a dry run first
sudo kubeadm upgrade diff v1.37.2
sudo kubeadm upgrade apply v1.37.2 --dry-run
# The real thing
sudo kubeadm upgrade apply v1.37.2
# [upgrade] SUCCESS! A control plane node of your cluster was upgraded to "v1.37.2".
upgrade apply also renews the kubeadm-managed certificates on this node unless you pass --certificate-renewal=false. Because this upgrade moves etcd from 3.6 to 3.7, in-flight API requests stall briefly while the etcd static Pod restarts; the official doc suggests stopping kube-apiserver with SIGTERM about 20 seconds before apply if that matters to you. After it finishes, upgrade your CNI plugin if its own release notes call for it.
Step 6: Upgrade the other control plane nodes with kubeadm upgrade node
On each remaining control plane node, one at a time, upgrade kubeadm the same way and run upgrade node instead of apply. You do not need upgrade plan here.
sudo apt-mark unhold kubeadm && \
sudo apt-get update && sudo apt-get install -y kubeadm='1.37.2-1.1' && \
sudo apt-mark hold kubeadm
sudo kubeadm upgrade node
# [upgrade/control-plane] The control plane instance for this node was successfully upgraded!
kubeadm applies the new CoreDNS and kube-proxy manifests only after the last control plane instance has been upgraded, which is why the order matters. Do not start the last control plane node until the others are finished.
Step 7: Drain, upgrade the kubelet, and uncordon each control plane node
A minor kubelet upgrade must happen on a drained node; in-place minor upgrades of a running kubelet are not supported. Run the drain from your workstation and the package commands on the node.
# Workstation
kubectl drain cp-1 --ignore-daemonsets --delete-emptydir-data
# On cp-1
sudo apt-mark unhold kubelet kubectl && \
sudo apt-get update && sudo apt-get install -y kubelet='1.37.2-1.1' kubectl='1.37.2-1.1' && \
sudo apt-mark hold kubelet kubectl
sudo systemctl daemon-reload
sudo systemctl restart kubelet
systemctl is-active kubelet
# active
# Workstation
kubectl uncordon cp-1
--delete-emptydir-data is needed when Pods use emptyDir volumes, and that data is lost on eviction, so make sure nothing important lives there. Repeat for each control plane node.
Step 8: Upgrade the worker nodes one at a time
Workers follow the Upgrade Linux nodes task: kubeadm and kubectl first, kubeadm upgrade node to refresh the kubelet configuration, then drain, kubelet, restart and uncordon. Do one node, or a small batch, at a time so you never drop below the capacity your workloads need.
# On worker-1
sudo apt-mark unhold kubeadm kubectl && \
sudo apt-get update && sudo apt-get install -y kubeadm='1.37.2-1.1' kubectl='1.37.2-1.1' && \
sudo apt-mark hold kubeadm kubectl
sudo kubeadm upgrade node
# Workstation
kubectl drain worker-1 --ignore-daemonsets --delete-emptydir-data
# On worker-1
sudo apt-mark unhold kubelet && sudo apt-get install -y kubelet='1.37.2-1.1' && sudo apt-mark hold kubelet
sudo systemctl daemon-reload && sudo systemctl restart kubelet
# Workstation
kubectl uncordon worker-1
Step 9: Verify the cluster
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# cp-1 Ready control-plane 1y v1.37.2
# worker-1 Ready <none> 1y v1.37.2
kubectl -n kube-system get pods -o wide | grep -Ev 'Running|Completed' || echo "all system pods healthy"
kubectl get --raw='/readyz?verbose' | tail -3
sudo kubeadm certs check-expiration | head -15
Once you are happy, clean up /etc/kubernetes/tmp on each control plane node (kubeadm leaves its backup folders there) and keep your off-node etcd snapshot until the next upgrade.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
upgrade plan says the target is not available, or apt only offers 1.36.x |
Repository still points at /v1.36/, or a mirror lags |
Repeat Step 4 on that node, run apt-get update, check apt-cache madison kubeadm |
Specified version to upgrade to ... is too high; kubeadm can upgrade only 1 minor version at a time |
Trying to skip a minor (for example 1.35 to 1.37) | Upgrade to the latest 1.36 patch first, then to 1.37 |
kubectl drain loops on Cannot evict pod as it would violate the pod's disruption budget |
A PodDisruptionBudget allows zero disruptions | Scale the workload up or relax the PDB temporarily; do not use --disable-eviction in production |
Drain refuses with cannot delete Pods with local storage (use --delete-emptydir-data to override) |
emptyDir volumes present | Add --delete-emptydir-data after confirming the data is disposable |
| kubelet fails to start after upgrade, log mentions cgroup v1 | Node is still on cgroup v1 and failCgroupV1 defaults to true |
Move the node to a cgroup v2 OS; set failCgroupV1: false in KubeletConfiguration only as a short-term override |
| kubelet exits with unknown flag errors | Deprecated cAdvisor flags still set in /var/lib/kubelet/kubeadm-flags.env or a drop-in |
Remove the flags; 1.37 accepts only --housekeeping-interval from that set |
kubeadm upgrade apply fails part way and does not roll back |
Interrupted run or a static Pod that never became healthy | Re-run the same command (it is idempotent), or kubeadm upgrade apply --force; manifests are backed up in /etc/kubernetes/tmp |
| kubeadm rejects your config file with an unknown API version | File uses kubeadm.k8s.io/v1beta3, removed in 1.37 |
Convert it with kubeadm config migrate using the 1.36 binary |
| Pods on SELinux nodes fail to mount shared volumes after upgrade | SELinuxMount went GA and on by default in 1.37 | Follow the Kubernetes SELinux volume labelling blog to set seLinuxChangePolicy or labels per Pod |
| kube-proxy logs a deprecation warning at startup | Running ipvs mode, or mode left empty |
Plan a move to nftables; set mode explicitly in the kube-proxy ConfigMap |
FAQ
Can I upgrade kubeadm from 1.35 directly to 1.37?
No. kubeadm allows one minor version per control plane upgrade and refuses anything larger. Upgrade the whole cluster to the latest 1.36 patch, then repeat the process for 1.37. Kubelets can technically stay behind for a while, but upgrade them on each pass so you do not accumulate skew.
Does kubeadm upgrade back up etcd automatically?
Partly. During upgrade apply and upgrade node, kubeadm writes a copy of the local etcd member data and the old static Pod manifests to /etc/kubernetes/tmp. That copy lives on the same node and is not a substitute for an etcdctl snapshot save file stored somewhere else.
Do I need to drain control plane nodes?
Yes for a minor kubelet upgrade. Control plane nodes often run CoreDNS and other critical Pods, and the kubeadm docs require draining any node before its kubelet moves a minor version. Patch upgrades are less strict, but draining is still the safe default.
How do I roll back a failed kubeadm upgrade?
There is no kubeadm downgrade. If the control plane upgrade fails, kubeadm attempts an automatic rollback of the static Pods; if that does not work, restore the manifests from /etc/kubernetes/tmp/kubeadm-backup-manifests-* and, as a last resort, restore etcd from your snapshot with etcdutl snapshot restore. This is why the off-node snapshot in Step 2 exists.
What I check first, every time, is the boring list: the off-node etcd snapshot actually opens with etcdutl snapshot status, no node still runs cgroup v1 or containerd 1.x, and no PDB allows zero disruptions. Then I rehearse the exact commands on a staging cluster and run production one node at a time with someone watching the dashboards. The managed equivalent for AWS is the EKS upgrade guide, and if you still run ingress-nginx, the Gateway API migration guide is worth doing before the next hop. Official references: Upgrading kubeadm clusters, Upgrade Linux nodes, Version skew policy and Kubernetes v1.37 release notes.
Related: How to Upgrade an EKS Cluster to 1.37 Safely, How to Back Up and Restore Kubernetes with Velero 1.18 and How to Fix Kubernetes Pods Stuck in Pending.
Spot an error or something out of date? Email me and I will fix it and log it on the corrections page.
I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.
Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.
Related
How to Fix AWS ALB Unhealthy Targets and 502/503 Errors (2026)
Fix AWS Application Load Balancer unhealthy targets and 502, 503 and 504 errors step by step: read the health reason code, fix health check paths and success codes, security groups, ip vs instance targets, EKS readiness gates, deregistration delay, keep-alive timeouts and access logs.
13 minNov 10, 2026How to Fix a Kubernetes CronJob Not Running or Running Twice (2026)
A cause-by-cause guide to Kubernetes CronJobs that never fire, fire late or fire twice: schedule and timeZone, suspend, startingDeadlineSeconds and the 100 missed schedules rule, concurrencyPolicy, BackoffLimitExceeded, history limits and kubectl create job --from.
11 minNov 10, 2026How to Fix a Kubernetes Service Connection Refused or Timeout (2026)
Step-by-step fix for a Kubernetes Service that is not reachable: tell connection refused from timeout, then check selectors and labels, empty EndpointSlices, targetPort vs containerPort, readiness, NetworkPolicy and kube-proxy, using netshoot and kubectl debug on Kubernetes 1.37.
11 min