My homelab's first real outage, and why the fix took 5 minutes

My homelab's first serious hardware failure: a disk that died slowly and quietly under a Kubernetes worker. It's the outage I'd been preparing for, and it felt great to see all that SRE work pay off. 🎉

Summary

Impact

WhatDownEffect
worker-node222:03–22:31, 22:51–22:58Paused in io-error; later a network drop
Apps on node2 (Jellyfin, *arr, …)22:03–22:35, 22:51–23:05Stuck Pending: node1 alone lacks the CPU
Postgres standby22:03–22:47Only replica lost; rebuilt from the primary
Cloudflare tunnel (LXC)22:30–22:55Disk on the same drive; recreated
Longhorn volumes22:03–23:05Degraded, served from node1's copies

Background

Each Kubernetes worker is a Proxmox VM with a second, 128 GB disk at /mnt/data for Longhorn replicas. Most volumes keep one replica per worker; CloudNativePG volumes keep one in total, since CNPG replicates itself. worker-node2's data disk sat on external-lvm, a thin pool on a 1 TB 2.5" USB hard drive.

Timeline

Weeks before Oct 22, night Sep 5 Oct 22 22:00 22:30 23:00 Backups worker-node2 Longhorn volumes Apps on node2 Postgres standby Cloudflare tunnel 22:03 node2 NotReady 22:47 standby ok 22:30 SSD swapped in 23:05 healthy 22:51 cable knocked down degraded or failing (node2 disk errors since 18:00)
TimeEvent
09-05First failed Proxmox backup of a container on the same drive. No alert.
09-12First failed Longhorn backup of a volume on node2. No alert.
about 18:00node2 logs medium errors; ext4 remounts a volume read-only.
22:03node2 stops responding: NotReady, VM paused in io-error.
22:30Disk swapped for a 1 TB SSD; node2 back at 22:31.
22:35Longhorn rebuilds replicas onto the new disk.
22:47CloudNativePG rebuilds the standby from the primary.
22:51pve2's Ethernet cable gets knocked while unplugging the old drive; node2 drops out, VM restarted.
23:05All volumes healthy, all pods running.

Root cause

The USB drive developed unreadable sectors:

critical medium error, dev sdb, sector 61086440 op 0x0:(READ)

Longhorn saw these as no data available. Once enough reads failed, QEMU paused the VM and Kubernetes lost the node.

Contributing factors:

Detection

Me, noticing Jellyfin was down. Nothing alerted: not the node going NotReady, and none of the earlier signals.

Resolution

Swap the disk, keep every name

The SSD got the old drive's exact layout, so IaC, Proxmox, fstab and Longhorn needed no changes:

qm stop 202
qm set 202 --delete scsi2                    # detach the old data disk
umount /mnt/dirstore
vgchange -an external-lvm
vgrename external-lvm external-lvm-old       # keep the old drive, don't wipe it
pvcreate /dev/sdc
vgcreate external-lvm /dev/sdc               # same VG name on the SSD
lvcreate -L 128G -n dirstore external-lvm
mkfs.ext4 -q /dev/external-lvm/dirstore
lvcreate -l 99%FREE --thinpool data external-lvm
mount /mnt/dirstore
qm set 202 --scsi2 external-lvm:128,iothread=1
qm start 202

Give Longhorn the old disk's identity

Longhorn tracks a disk by longhorn-disk.cfg at its root, so it kept waiting for the old one, and refused to remove it while stale replicas were listed. After deleting the dead replica records:

echo '{"diskName":"default-disk-23c2c7425e81b15d","diskUUID":"<old disk UUID>","diskDriver":"","state":"ready"}' \
  | sudo tee /mnt/data/longhorn-disk.cfg

Longhorn saw an empty, ready disk and rebuilt every replica from node1. Not an official procedure, but fine on an empty disk.

Let CloudNativePG heal Postgres

Deleting the dead standby's PVC and pod was enough; CNPG cloned a new one from the primary.

What went well

What went wrong

Take home