Longhorn is a CNCF distributed block storage system for Kubernetes. It turns the local disks of your nodes into replicated persistent volumes: each volume keeps copies on several nodes, so a pod can be rescheduled to another node and keep its data after a node failure. In this tutorial you will prepare Ubuntu 24.04 nodes, install Longhorn with Helm, create volumes with a custom StorageClass, expand one online and configure scheduled backups to S3-compatible object storage.
Prerequisites
To follow this tutorial, you will need:
- A Kubernetes cluster (1.25 or newer) with at least three worker nodes running Ubuntu 24.04, for example CubePath VPS instances. Longhorn recommends 4 vCPUs and 4 GB of RAM per node for production; 2 vCPUs and 4 GB work for testing.
- Free disk space on each node under
/var/lib/longhorn, the default data path. Every replica of a volume uses space on a different node. - SSH access with a sudo user to every node, and
kubectlplus Helm 3 on the machine you manage the cluster from. - For Step 6: an S3-compatible bucket and an access key with read and write permissions on it.
Step 1 - Preparing the nodes
Longhorn attaches volumes to pods over iSCSI, so every node needs the iSCSI initiator. The nfs-common package is only needed for ReadWriteMany volumes, but installing it now avoids surprises later. Run these commands on every node:
sudo apt update
sudo apt install -y open-iscsi nfs-common
sudo systemctl enable --now iscsid
sudo modprobe iscsi_tcp
Make the kernel module load at boot:
echo iscsi_tcp | sudo tee /etc/modules-load.d/iscsi_tcp.conf
Verify the daemon is running:
systemctl is-active iscsid
active
Ubuntu Server enables multipathd, which claims the block devices Longhorn creates and makes volumes fail to mount with already mounted or mount point busy. Unless you use multipath for other storage, tell it to ignore regular SCSI devices. Open the configuration file:
sudo nano /etc/multipath.conf
Add this block at the end of the file:
blacklist {
devnode "^sd[a-z0-9]+"
}
Restart the service and confirm it reads the new configuration without errors:
sudo systemctl restart multipathd
systemctl status multipathd --no-pager
Step 2 - Installing Longhorn with Helm
From your workstation, add the Longhorn chart repository and install it into its own namespace:
helm repo add longhorn https://charts.longhorn.io
helm repo update
helm install longhorn longhorn/longhorn \
--namespace longhorn-system \
--create-namespace
To pin a version, list the available ones with helm search repo longhorn/longhorn --versions and add --version. Longhorn only supports upgrading one minor version at a time, so note which version you install.
The first start takes a few minutes while images are pulled on each node. Watch the pods until they are all Running:
kubectl -n longhorn-system get pods -w
NAME READY STATUS RESTARTS AGE
csi-attacher-5c4bfdcf59-7hkqz 1/1 Running 0 3m
csi-provisioner-667796df57-wx9bq 1/1 Running 0 3m
engine-image-ei-db6c2b6f-2lmfv 1/1 Running 0 4m
instance-manager-4c1a8a0d7c7e3f3e2b1d9c8f6a5e4d3c 1/1 Running 0 4m
longhorn-csi-plugin-6jg8p 3/3 Running 0 3m
longhorn-driver-deployer-7d8b8c9f4d-bz2kx 1/1 Running 0 5m
longhorn-manager-5vxrl 2/2 Running 0 5m
longhorn-ui-6f5d8c7b9d-hq4tn 1/1 Running 0 5m
Press Ctrl+C to stop watching. The chart also creates a StorageClass called longhorn that keeps three replicas per volume and is marked as the cluster default:
kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
longhorn (default) driver.longhorn.io Delete Immediate true 5m
If another StorageClass is already the default, remove its storageclass.kubernetes.io/is-default-class annotation so only one default remains.
Check that Longhorn found all nodes and their disks:
kubectl -n longhorn-system get nodes.longhorn.io
NAME READY ALLOWSCHEDULING SCHEDULABLE AGE
k8s-w1 True true True 5m
k8s-w2 True true True 5m
k8s-w3 True true True 5m
Step 3 - Opening the Longhorn UI
The web UI shows volumes, replicas, node disk usage and backups. It has no authentication of its own, so do not expose the longhorn-frontend Service publicly without an authenticating Ingress. For administration, a port-forward is enough:
kubectl -n longhorn-system port-forward svc/longhorn-frontend 8080:80
Open http://localhost:8080 in your browser. If you run kubectl on a remote server, forward the port over SSH first with ssh -L 8080:localhost:8080 your_user@your_server_ip.
Step 4 - Creating a custom StorageClass
The default class is a good general choice. For workloads that replicate data themselves, such as a database cluster, two replicas and a preference for keeping one replica on the same node as the pod reduce overhead. Create the file:
nano longhorn-2r.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: longhorn-2r
provisioner: driver.longhorn.io
allowVolumeExpansion: true
reclaimPolicy: Retain
volumeBindingMode: Immediate
parameters:
numberOfReplicas: "2"
staleReplicaTimeout: "2880"
dataLocality: "best-effort"
fsType: "ext4"
reclaimPolicy: Retain keeps the Longhorn volume when the claim is deleted, which protects against an accidental kubectl delete pvc. staleReplicaTimeout is in minutes. Apply it:
kubectl apply -f longhorn-2r.yaml
Step 5 - Creating and expanding a volume
Create a PersistentVolumeClaim and a pod that writes to it:
nano longhorn-test.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-test
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn-2r
resources:
requests:
storage: 2Gi
---
apiVersion: v1
kind: Pod
metadata:
name: volume-test
spec:
containers:
- name: app
image: busybox
command: ["sh", "-c", "date >> /data/log.txt && sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: data-test
kubectl apply -f longhorn-test.yaml
kubectl get pvc data-test
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-test Bound pvc-3f6a1c2e-8b1d-4d7a-9c4e-1a2b3c4d5e6f 2Gi RWO longhorn-2r 20s
Confirm the data landed on the volume:
kubectl exec volume-test -- cat /data/log.txt
The Longhorn volume object shows its state and robustness. healthy means all replicas are in sync:
kubectl -n longhorn-system get volumes.longhorn.io
NAME DATA ENGINE STATE ROBUSTNESS SCHEDULED SIZE NODE AGE
pvc-3f6a1c2e-8b1d-4d7a-9c4e-1a2b3c4d5e6f v1 attached healthy 2147483648 k8s-w2 1m
Longhorn supports online expansion. Increase the request on the claim to 5 GiB:
kubectl patch pvc data-test -p '{"spec":{"resources":{"requests":{"storage":"5Gi"}}}}'
After a few seconds the filesystem inside the pod reflects the new size:
kubectl exec volume-test -- df -h /data
Filesystem Size Used Available Use% Mounted on
/dev/longhorn/pvc-3f6a1c2e-8b1d-4d7a-9c4e-1a2b3c4d5e6f
4.9G 24.0K 4.9G 0% /data
Volumes can only grow; you cannot shrink a PVC.
Step 6 - Configuring backups to S3
Snapshots live on the same nodes as the volume, so they do not protect you from losing the cluster. Backups copy volume data to an external backup target. Create a secret with the S3 credentials in the longhorn-system namespace. AWS_ENDPOINTS is only needed for S3-compatible providers other than AWS:
kubectl -n longhorn-system create secret generic longhorn-s3-secret \
--from-literal=AWS_ACCESS_KEY_ID=your_access_key \
--from-literal=AWS_SECRET_ACCESS_KEY=your_secret_key \
--from-literal=AWS_ENDPOINTS=https://your_s3_endpoint
Point the default backup target at your bucket. The URL format is s3://bucket_name@region/optional_prefix/:
kubectl -n longhorn-system patch backuptargets.longhorn.io default --type merge \
-p '{"spec":{"backupTargetURL":"s3://your_bucket@us-east-1/longhorn/","credentialSecret":"longhorn-s3-secret"}}'
You can do the same from the UI under Settings > Backup Target. Verify that Longhorn can reach the bucket:
kubectl -n longhorn-system get backuptargets.longhorn.io
NAME URL CREDENTIAL LASTBACKUPAT AVAILABLE LASTSYNCEDAT
default s3://your_bucket@us-east-1/longhorn/ longhorn-s3-secret 5m0s true 2026-09-25T10:20:11Z
Now schedule a nightly backup with a RecurringJob. Jobs in the default group apply to every volume that has no other recurring job assigned:
nano backup-daily.yaml
apiVersion: longhorn.io/v1beta2
kind: RecurringJob
metadata:
name: backup-daily
namespace: longhorn-system
spec:
task: backup
cron: "0 2 * * *"
groups:
- default
retain: 7
concurrency: 2
This keeps the last seven daily backups of each volume and runs at most two backups at a time. Apply it and check that it is registered:
kubectl apply -f backup-daily.yaml
kubectl -n longhorn-system get recurringjobs.longhorn.io
NAME GROUPS TASK CRON RETAIN CONCURRENCY AGE LABELS
backup-daily ["default"] backup 0 2 * * * 7 2 10s
To test the target without waiting for the schedule, open the volume in the UI and click Create Backup. The backup appears on the Backup page, and from there you can restore it to a new volume, even on a different cluster configured with the same backup target.
Troubleshooting
Volume stuck in attaching or the pod reports MountVolume.MountDevice failed. Check that iscsid is active on the node running the pod and that multipathd is not holding the device (Step 1). kubectl describe pod your_pod shows the exact error.
Volume shows degraded. One replica is missing, usually because a node is down or its disk is full. Longhorn rebuilds the replica automatically when a node with space is available. Check disk usage per node in the UI or with kubectl -n longhorn-system get nodes.longhorn.io -o wide.
Backup target shows AVAILABLE false. The credentials, endpoint or bucket name are wrong. Read the error in kubectl -n longhorn-system describe backuptargets.longhorn.io default.
Conclusion
You prepared Ubuntu 24.04 nodes for Longhorn, installed it with Helm, created replicated volumes through a custom StorageClass, grew a volume online and scheduled daily backups to S3. Next, you can add a snapshot recurring job for frequent local restore points, deploy the snapshot controller to use Kubernetes VolumeSnapshot objects with Longhorn, or scrape the longhorn-backend metrics endpoint with Prometheus to alert on degraded volumes.
