K3s 1.35 dropped on January 15, 2026, tracking upstream Kubernetes “Timbernetes.” Most of the release notes are the usual CRI/CNI/etcd churn, but two features are worth upgrading today for:
- In-place pod resize is now GA — bump CPU/memory on a running pod without restarting it.
- Image volumes are Stable — mount an OCI image as a read-only volume, so you can ship config blobs or model weights without baking them into container layers.
For homelab use, those are both real quality-of-life wins. This post is the upgrade from 1.34 on a 3-node cluster, plus a demo of each feature.
What’s in the Release
The headline changes worth flagging before you upgrade:
- Built on Go 1.25.
- Streaming protocol moved from SPDY → WebSockets. Affects
kubectl exec,port-forward, andattach. Old kubectl clients may misbehave — upgrade kubectl too. - CNI startup refactored into a first-class
Executorinterface. Shouldn’t matter for the stock Flannel/Calico users but watch your logs on first boot. - Default branch of the K3s repo renamed
master→main. Only matters if you’re pinning to a branch. - In-place
resources.resize→ GA. imagevolume type → Stable.
Prerequisites
- A K3s 1.34.x cluster (server + one or more agents)
- Root SSH to every node
kubectlandetcdctlhandy- A recent etcd snapshot — this is non-negotiable
Take the snapshot first:
sudo k3s etcd-snapshot save --name pre-135
On an Older Minor? Hop, Don’t Jump
Kubernetes — and therefore K3s — only supports upgrading one minor version at a time. Going directly from 1.32 to 1.35 skips two minors and risks broken API versions, kubelet↔control-plane incompatibility, and etcd schema migrations that assume the intermediate hops happened.
If you’re on 1.33 or earlier, do sequential hops first and come back to this guide for the final 1.34 → 1.35 step:
curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=v1.33 sh - # then v1.34, then v1.35
Between every hop: verify kubectl get nodes is Ready, confirm nothing’s CrashLoopBackOff with kubectl get pods -A, and take a fresh etcd snapshot. Two things to watch for along the way:
- 1.32 → 1.33:
autoscaling/v2beta*andflowcontrol.apiserver.k8s.io/v1beta3finished removal. Re-apply any manifests still referencing them against the current API groups. - 1.34 → 1.35: The streaming protocol flips from SPDY to WebSockets. Upgrade your local
kubectltoo orexec/port-forwardwill misbehave.
A full 1.32 → 1.35 run takes ~15 minutes on a small homelab cluster.
HA Cluster? Roll, Don’t Stop
If you’re running multi-server HA — three (or more) nodes each running control plane + workload, sharing embedded etcd — you do not need to shut anything down. That’s the reason you built HA in the first place. K3s embedded etcd tolerates one server being offline at a time because quorum on a 3-node cluster is 2, so while one node is bouncing the other two keep serving the API and scheduling pods.
Per-node procedure, one node at a time:
# On any node, first:
sudo k3s etcd-snapshot save --name pre-135
# Then for each of the three nodes in turn:
kubectl cordon <node-name> # stop scheduling new pods here
curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=v1.35 sh -
# wait for:
kubectl get nodes # should show <node-name> Ready
kubectl get pods -A | grep -v Running # should be empty
kubectl uncordon <node-name>
The installer replaces the binary and restarts the k3s systemd unit in ~30–90 seconds. Workloads running on that node will have a brief interruption; if that matters for a specific deployment, add a kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data between cordon and the installer so pods migrate off first.
Do not run the installer on two servers in parallel. Losing two of three servers at once means losing etcd quorum, and the cluster freezes until the first one comes back.
Once all three are on 1.35, verify:
sudo k3s kubectl get nodes -o wide
sudo k3s kubectl version
If you’re on this topology, skip Step 1 and Step 2 below — they cover the single-server + agents topology — and go straight to the feature demos.
Step 1: Upgrade the Server
On the control-plane node:
curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=v1.35 sh -
The installer stops the old service, drops the new binary in place, and restarts. Give it ~60 seconds and check:
sudo k3s kubectl get nodes
sudo k3s kubectl version
Server should report v1.35.x+k3s1 and all nodes should still be Ready. Agents will keep running the old version until you upgrade them — that’s fine, K3s supports a one-minor-version skew.
Step 2: Upgrade the Agents
On each agent, one at a time:
curl -sfL https://get.k3s.io | \
INSTALL_K3S_CHANNEL=v1.35 \
K3S_URL=https://<server-ip>:6443 \
K3S_TOKEN=<your-token> \
sh -
Watch with kubectl get nodes -w from the server. When it flips back to Ready, move on to the next agent.
Demo 1: In-Place Pod Resize
Deploy a Pi-hole pod with modest memory, then bump it live. Deployment:
apiVersion: apps/v1
kind: Deployment
metadata: { name: pihole }
spec:
replicas: 1
selector: { matchLabels: { app: pihole } }
template:
metadata: { labels: { app: pihole } }
spec:
containers:
- name: pihole
image: pihole/pihole:2026.02
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }
resizePolicy:
- resourceName: memory
restartPolicy: NotRequired
The resizePolicy block with NotRequired is what permits an in-place resize. Apply it, wait for the pod to come up, then bump memory:
kubectl patch pod <pihole-pod> --subresource resize --patch '
{"spec":{"containers":[{"name":"pihole",
"resources":{
"requests":{"memory":"256Mi"},
"limits":{"memory":"512Mi"}}}]}}'
Describe the pod — RestartCount stays 0 and Status.Resources reflects the new limits. No DNS blip, no Pi-hole restart, no GRUB of angry family members.
Demo 2: OCI Image Volume
Say you want to ship a shared config bundle across several workloads without baking it into each container. Build a tiny OCI image with just the config:
cat <<'EOF' > Dockerfile.config
FROM scratch
COPY app-config.json /config/
EOF
docker buildx build -f Dockerfile.config -t ghcr.io/timothydodd/homelab-config:v1 --push .
Mount it as a volume:
spec:
containers:
- name: app
image: my-app:latest
volumeMounts:
- name: shared-config
mountPath: /etc/app
readOnly: true
volumes:
- name: shared-config
image:
reference: ghcr.io/timothydodd/homelab-config:v1
pullPolicy: IfNotPresent
Kubelet pulls the image via the CRI, exposes its filesystem read-only, and the pod sees /etc/app/app-config.json. Rev the config? Push a new image tag and redeploy. Works for model weights, static assets, certificate bundles — anywhere you’d otherwise reach for a ConfigMap + base64 hack.
Rollback Plan
If something goes sideways, K3s keeps the previous binary around:
sudo /usr/local/bin/k3s-uninstall.sh # agents: k3s-agent-uninstall.sh
curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=v1.34 sh -
sudo k3s server --cluster-init --etcd-snapshot-file=<your-snapshot>
I haven’t needed it yet on a 1.34→1.35 hop but snapshot or pray.
Takeaways
In-place resize alone was worth the upgrade — I’ve been restarting Pi-hole every time I wanted to give it more memory for its cache, and that’s a solved problem now. Image volumes take a beat to click, but once they do you’ll find config sprawl disappearing.
Next up: wiring the image-volume pattern into my Ollama deployments so model weights are a pinned OCI reference instead of a 40 GB persistent volume I have to hand-manage. That’s a follow-up post.