Restoring from Longhorn Backups
How to restore Kubernetes applications from Longhorn backups — scale down, remove empty PVCs, restore volumes, create PVs/PVCs, and bring everything back up.
I redeployed my K8s cluster with Flux and got back a perfectly healthy cluster with nothing in it. Every app running, every volume empty — no configs, no dashboards, no media metadata. This is what it took to get six applications back from Longhorn backups.
| Application | Size | Namespace |
|---|---|---|
| Prometheus | 45GB | observability |
| Loki | 20GB | observability |
| Grafana | 10GB | observability |
| Jellyfin | 10GB | default |
| qBittorrent | 5GB | default |
| Sonarr | 5GB | default |
About 30 minutes end to end.
You’ll need: kubectl access, Longhorn installed and connected to your S3 backend, and the Longhorn UI reachable.
Note (Updated June 2026)
Two things have changed since. The backup set is smaller — only the media and *arr config volumes are backed up now, opted in by PVC label, while Prometheus, Loki and Grafana deliberately start empty on a restore. And the whole flow below is task longhorn:restore in the infrastructure repo. The manual procedure still holds for restoring any single volume by hand. Current shape: homelab tour — Backup.
The backend also moved from MinIO to Garage in October 2025, after MinIO dropped their Docker images. Nothing below changes — Longhorn only ever sees S3. That migration is its own post.
Step 1 — Look before you touch
kubectl get deployments -Akubectl get statefulsets -Akubectl get pvc -A -o widekubectl get storageclassYou want the exact PVC names before you delete any of them. StatefulSet PVCs in particular are generated, not authored, and prometheus-kube-prometheus-stack-db-prometheus-kube-prometheus-stack-0 is not a name you’ll reconstruct from memory.
Step 2 — Scale everything down
Caution
Every workload touching a volume must be at zero replicas before you go near storage. A pod still writing to a PVC you’re about to delete is how a restore turns into a data loss incident.
kubectl scale deployment jellyfin --replicas=0 -n defaultkubectl scale deployment qbittorrent --replicas=0 -n defaultkubectl scale deployment sonarr --replicas=0 -n defaultkubectl scale deployment grafana --replicas=0 -n observability
kubectl scale statefulset loki --replicas=0 -n observabilitykubectl scale statefulset prometheus-kube-prometheus-stack --replicas=0 -n observabilitykubectl scale statefulset alertmanager-kube-prometheus-stack --replicas=0 -n observabilityWait for the pods to actually terminate. --replicas=0 returns immediately; the pods do not.
Step 3 — Delete the empty PVCs
They hold nothing, and they’re what’s standing between the restored volumes and the names the apps expect:
kubectl delete pvc jellyfin -n defaultkubectl delete pvc qbittorrent -n defaultkubectl delete pvc sonarr -n default
kubectl delete pvc grafana -n observabilitykubectl delete pvc storage-loki-0 -n observabilitykubectl delete pvc prometheus-kube-prometheus-stack-db-prometheus-kube-prometheus-stack-0 -n observabilityStep 4 — Restore the volumes
Longhorn UI → Backup tab → the ⟲ restore button on each backup:
- Name:
<app>-restored, e.g.jellyfin-restored - Storage Class:
longhorn - Access Mode:
ReadWriteOnce
The name matters — it becomes the volumeHandle in the next step, and there’s no rename afterwards.
Warning
Let every restore finish before continuing. A 45GB Prometheus volume takes a while, and a PV pointing at a half-restored volume binds cleanly and then fails at attach time with an error about replicas.
Step 5 — A PV per restored volume
Adjust storage and volumeHandle for each app:
# Repeat for each applicationapiVersion: v1kind: PersistentVolumemetadata: name: jellyfin-restored-pvspec: capacity: storage: 10Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: longhorn csi: driver: driver.longhorn.io fsType: ext4 volumeAttributes: numberOfReplicas: "3" staleReplicaTimeout: "30" volumeHandle: jellyfin-restoredpersistentVolumeReclaimPolicy: Retain is deliberate. On Delete, removing the PVC again during the same session takes the restored data with it.
Step 6 — A PVC bound to it by name
volumeName is what stops the scheduler from helpfully provisioning a fresh empty volume instead:
# Repeat for each applicationapiVersion: v1kind: PersistentVolumeClaimmetadata: name: jellyfin namespace: defaultspec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi storageClassName: longhorn volumeName: jellyfin-restored-pvThe PVC name has to be the original one the app asks for, not jellyfin-restored.
Step 7 — Check the binding, then scale up
kubectl get pvc -n default | grep -E "(jellyfin|qbittorrent|sonarr)"kubectl get pvc -n observability | grep -E "(grafana|storage-loki|prometheus)"kubectl get volumes -n longhorn-system | grep "restored"Every PVC Bound, every Longhorn volume attached and healthy. Anything still Pending is a name mismatch between the PVC and the PV, and scaling up on top of it will only bury the error under a CrashLoopBackOff:
kubectl scale deployment jellyfin --replicas=1 -n defaultkubectl scale deployment qbittorrent --replicas=1 -n defaultkubectl scale deployment sonarr --replicas=1 -n defaultkubectl scale deployment grafana --replicas=1 -n observability
kubectl scale statefulset loki --replicas=1 -n observabilitykubectl scale statefulset prometheus-kube-prometheus-stack --replicas=1 -n observabilitykubectl scale statefulset alertmanager-kube-prometheus-stack --replicas=1 -n observabilitykubectl get pods -A | grep -v Running | grep -v Completedkubectl get pods -n default -o widekubectl get pods -n observability -o wideWithout the UI
The same restore as a CRD, if the UI is down or you’re scripting it:
apiVersion: longhorn.io/v1beta1kind: Volumemetadata: name: jellyfin-restored namespace: longhorn-systemspec: size: "10737418240" restoreVolumeRecurringJob: false fromBackup: "s3://your-minio-bucket/backups/backup-name"The seven steps above are what the automation replaced. Writing them out by hand once is still worth it — when the automated restore fails, this is the layer you drop back to, and half an hour of kubectl beats reconstructing the order under pressure.