Longhorn#
Everything we’ve done with storage so far lived on one node. In the v1 notes a PVC was a local disk on (the only) Minikube node, and in the multi-node notes our workers each keep their own local directories. That means the day kworker1 dies, every PVC hosted on it dies with it - block storage was the last single point of failure in our homelab.
Longhorn fixes that: it’s a distributed block storage system built as a CSI driver, purpose-made for Kubernetes. Every volume it serves is split into an engine plus N replicas (default 3) that live on different nodes - scheduled exactly like pods are.
The Architecture#
Three pieces matter:
- longhorn-manager - a DaemonSet that owns all the bookkeeping (volumes, replicas, settings) and talks to the Kubernetes API
- longhorn-driver - the CSI plugin. Pods request volumes through PVCs; this is the component that creates/attaches them
- instance-manager pods - the heavy lifters. Each volume gets an engine process (one per volume) and its replicas run here, distributed across nodes
The data path for a single write:
pod → CSI attach (iSCSI) → longhorn engine → write to all replicas → ACKReads are served by the engine from the replicas. The engine/replica split is Longhorn’s signature: when the node hosting the engine dies, a new engine is started on another node and replicas are reattached. When a replica’s node dies, the remaining replicas keep serving and Longhorn starts rebuilding a new replica in the background.
Replication here is synchronous at the block layer: the engine considers a write done once it’s persisted on the replicas. This is protection against node/disk loss - and it is not the same thing as PostgreSQL’s streaming replication, which we’ll layer on top in the next chapters.
Snapshot vs Backup#
The distinction that saves you from a false sense of safety:
| Snapshot | Backup | |
|---|---|---|
| Nerede | Longhorn cluster’ın kendi node’larında | Dışarıda: S3/NFS (cluster dışı) |
| Ne korur | Yanlış silme, bad upgrade sonrası geri dönüş | Tüm cluster’ın kaybı |
| Nasıl | CoW anlık görüntü | Tarayıcı (sync agent) ile uzun süreli dosya |
özetle: snapshot’lar cluster ile birlikte yaşar - cluster yanarsa snapshot da yanar. Gerçek backup her zaman S3/NFS’e. Homelab’da en azından snapshot + offsite backup kombinasyonunu düşün.
The Honest Requirements#
- iscsid on every node - the CSI attachment path is iSCSI-based. Missing
iscsidis the #1 “volumes stuck in attaching” cause - Enough free disk under
/var/lib/longhornon each node (the default data location) - A Linux kernel ≥ 4.18 (5.8+ recommended)
- NFS client only if you want RWX volumes (the NFS-server-based sharing layer)
- A real multi-node cluster. On Minikube’s single node Longhorn installs and runs, but replicas collapse onto one node: zero redundancy. Test it there only to see the UI; this chapter assumes the 4-node cluster from the multi-node notes
How to Prepare the Nodes#
- On every node (all four), install the iSCSI pieces:
sudo apt-get install -y open-iscsi
sudo systemctl enable --now iscsid
sudo systemctl status iscsid --no-pagerDo this before installing Longhorn. If
iscsidis missing, the CSI driver installs fine and then the first volume attach hangs forever - aPendingthat reads like a scheduling bug but isn’t. This is exactly the kind of trap the troubleshooting checklist is for.
- Check disk headroom on each node - Longhorn stores replicas here:
df -h /var/lib/longhorn 2>/dev/null || df -h /- If
multipathdis running, make sure it won’t grab Longhorn’s devices (on stock Debian VMs it usually isn’t - verify rather than assume):
systemctl is-active multipathd || trueHow to Install Longhorn with Helm#
- Add the repo and install into a dedicated namespace:
helm repo add longhorn https://charts.longhorn.io
helm repo update
kubectl create namespace longhorn-system
helm install longhorn longhorn/longhorn --namespace longhorn-system --version 1.7.3- Wait for the fleet to come up (there are many pods; that’s normal):
kubectl -n longhorn-system get pods --watchYou want longhorn-manager pods on every node, longhorn-driver-deployer completed, and CSI pods (csi-attacher, csi-provisioner, longhorn-csi-plugin) running.
- Verify the StorageClass was created and check the UI:
kubectl get sc
kubectl -n longhorn-system port-forward svc/longhorn-frontend 8085:80Open http://localhost:8085 - the UI shows nodes, disks, volumes and their replica placement. Seeing the 3 replicas of a volume spread across your nodes is the “aha” moment of this chapter.
How to Make Longhorn the Default StorageClass#
Minikube already ships a default SC (standard); the kubeadm cluster from the multi-node notes starts with none until a driver claims the title. If some other class is already marked default, two defaults = new PVCs silently going to the wrong backend.
- Demote the old one and promote Longhorn:
kubectl patch sc standard -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
kubectl patch sc longhorn -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
kubectl get scThe default is the one with (default) suffix.
- Existing PVCs keep their original class - this only affects new claims. If you already have stateful workloads on the old SC, plan a migration instead of assuming a repatch moves them.
How to Test Node Failover with a Real Workload#
- Deploy a writer so we can watch the volume migrate:
apiVersion: apps/v1
kind: Deployment
metadata:
name: writer
spec:
replicas: 1
selector:
matchLabels:
app: writer
template:
metadata:
labels:
app: writer
spec:
containers:
- name: shell
image: busybox:1.36
command: ["sh", "-c", "echo $(date) hello >> /data/heartbeat.log && sleep 5"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: data-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-pvc
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 2Gi- Find where the volume lives, then take that node out of scheduling (simulating a failure, the polite way):
kubectl -n longhorn-system get volumes.longhorn.io
kubectl cordon <node-with-the-pod>
kubectl delete pod <writer-pod>
kubectl get pods -o wide --watch- Watch the sequence: new pod schedules on another node → Longhorn stops the engine on the old node, starts it on the new one, reattaches → pod starts,
/data/heartbeat.logcontinues from where it left off. In the UI you’ll see the volume flip nodes while its 3 replicas stay spread.
This is the Longhorn contract: the pod is scheduled anywhere, its block volume follows it. It’s not magic - it’s replicas + an engine that can move. Un-cordon the node afterwards (
kubectl uncordon <node>), or your next test surprises you.
- Also test the sad path once: stop a node hard (VM shutdown in Proxmox) instead of cordoning, and watch Longhorn mark the replica as failed and rebuild it when the node returns.
for more postgresql-statefulset-replication