DEV Community

Cover image for Installing Longhorn on Kubernetes
Sanskriti Harmukh for Vultr

Posted on with Aashish Chaurasiya Originally published at docs.vultr.com

Installing Longhorn on Kubernetes

Longhorn is an open-source, lightweight distributed block storage system for Kubernetes that provides persistent, highly available storage for stateful applications running in a cluster. It runs a CSI driver, an instance manager on every node, a storage engine that replicates data and creates snapshots/backups, and a web UI for managing volumes. This guide walks through installing Longhorn on a Kubernetes cluster, creating ReadWriteOnce (RWO) and ReadWriteMany (RWX) StorageClasses, provisioning and testing PVCs with sample pods, exposing the Longhorn UI externally through an Nginx Ingress controller, and configuring volume backups to S3-compatible object storage. By the end, you'll have Longhorn running as your cluster's distributed storage layer, with tested RWO and RWX volumes, a browser-accessible dashboard, and automated backups to external object storage.

Prerequisites: a Kubernetes cluster with at least 3 nodes (4 vCPUs each), S3-compatible object storage with a bucket (e.g. longhorn) for volume backups, a Linux management workstation with SSH access as a non-root sudo user, and kubectl installed and configured to reach your cluster.


1. Install Longhorn on Kubernetes

You can install Longhorn with Helm or kubectl. The steps below use kubectl with a pinned release manifest.

1. Download the latest Longhorn release manifest:

$ wget https://github.com/longhorn/longhorn/releases/download/v1.6.1/longhorn.yaml
Enter fullscreen mode Exit fullscreen mode

This downloads Longhorn v1.6.1 — check the GitHub repository for the latest version before deploying.

2. Verify the file downloaded:

$ ls
Enter fullscreen mode Exit fullscreen mode

Output:

longhorn.yaml
Enter fullscreen mode Exit fullscreen mode

3. Deploy Longhorn to your cluster:

$ kubectl apply -f longhorn.yaml
Enter fullscreen mode Exit fullscreen mode

A successful install creates CRDs, RBAC roles, services (longhorn-backend, longhorn-frontend, etc.), a longhorn-manager DaemonSet, and longhorn-driver-deployer/longhorn-ui deployments.

4. Verify all Longhorn resources are running in the new longhorn-system namespace:

$ kubectl get all -n longhorn-system
Enter fullscreen mode Exit fullscreen mode

Example output:

NAME                                                    READY   STATUS    RESTARTS      AGE
pod/csi-attacher-57689cc84b-tlzpb                       1/1     Running   0             73s
pod/longhorn-csi-plugin-lp562                           3/3     Running   1 (15s ago)   73s
pod/longhorn-ui-655b65f7f9-lgllp                        1/1     Running   0             98s

NAME                                  TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/longhorn-admission-webhook    ClusterIP   10.100.203.235   <none>        9502/TCP   103s
service/longhorn-backend              ClusterIP   10.110.4.220     <none>        9500/TCP   104s

NAME                                      DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
daemonset.apps/engine-image-ei-5cefaf2b   3         3         3       3            3           <none>          84s
daemonset.apps/longhorn-csi-plugin        3         3         3       3            3           <none>          74s
daemonset.apps/longhorn-manager           3         3         3       3            3           <none>          100s
Enter fullscreen mode Exit fullscreen mode

5. Install the Longhorn NFS package to enable ReadWriteMany (RWX) volumes:

$ kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.6.1/deploy/prerequisite/longhorn-nfs-installation.yaml
Enter fullscreen mode Exit fullscreen mode

6. Confirm the NFS installation pods are running:

$ kubectl get pods | grep longhorn-nfs-installation
Enter fullscreen mode Exit fullscreen mode

Output:

longhorn-nfs-installation-2kd6d   1/1     Running   0          43s
longhorn-nfs-installation-52kgg   1/1     Running   0          43s
longhorn-nfs-installation-qsst9   1/1     Running   0          43s
Enter fullscreen mode Exit fullscreen mode

2. Create Longhorn Storage Classes

Longhorn ships with a default longhorn StorageClass, but you'll typically create dedicated classes per access mode so PVCs can target the right one.

ReadWriteOnce (RWO) StorageClass

RWO mounts a volume with read-write access on a single pod at a time — suitable for workloads like databases that need consistent writes to a single writer.

1. Create the StorageClass file:

$ nano rwo-storageclass.yaml
Enter fullscreen mode Exit fullscreen mode

2. Add the following configuration:

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: longhorn-prod
provisioner: driver.longhorn.io
allowVolumeExpansion: true
parameters:
  numberOfReplicas: "3"
  staleReplicaTimeout: "2880" # 48 hours in minutes
  fromBackup: ""
  fsType: "ext4"
reclaimPolicy: Retain
Enter fullscreen mode Exit fullscreen mode

This creates an expandable StorageClass longhorn-prod with a Retain reclaim policy so the volume survives PVC deletion. Key fields:

  • provisionerdriver.longhorn.io routes provisioning through the Longhorn engine. A cloud block-storage CSI driver value here instead would create volumes that Longhorn can't manage.
  • allowVolumeExpansion — lets you grow a volume's capacity later (e.g. 10Gi → 20Gi).
  • numberOfReplicas — number of Longhorn volume replicas spread across nodes.
  • staleReplicaTimeout — marks a replica dormant after this many idle minutes.
  • fromBackup — restores the volume from a backup source (e.g. your object storage bucket).
  • fsType — filesystem format for the volume.
  • reclaimPolicyDelete or Retain behavior when the bound PVC/PV is removed.

3. Apply the StorageClass:

$ kubectl apply -f rwo-storageclass.yaml
Enter fullscreen mode Exit fullscreen mode

4. Verify it's available:

$ kubectl get storageclass
Enter fullscreen mode Exit fullscreen mode

Output:

NAME                                PROVISIONER           RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
longhorn (default)                  driver.longhorn.io    Delete          Immediate           true                   35m
longhorn-prod                       driver.longhorn.io    Retain          Immediate           true                   4s
standard                            block.csi.example.com Delete          Immediate           true                   46m
Enter fullscreen mode Exit fullscreen mode

(The standard row is just an example of a cluster's pre-existing default StorageClass — run kubectl get storageclass on your own cluster and substitute the real name/provisioner it shows.)

ReadWriteMany (RWX) StorageClass

RWX lets multiple pods mount the same volume with read-write access simultaneously.

1. Create the StorageClass file:

$ nano rwx-storageclass.yaml
Enter fullscreen mode Exit fullscreen mode

2. Add the following configuration:

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: longhorn-rwx
provisioner: driver.longhorn.io
allowVolumeExpansion: true
reclaimPolicy: Delete
volumeBindingMode: Immediate
parameters:
  numberOfReplicas: "3"
  staleReplicaTimeout: "2880"
  fromBackup: ""
  fsType: "ext4"
  nfsOptions: "vers=4.2,noresvport,softerr,timeo=600,retrans=5,rw,hard"
reclaimPolicy: Retain  
Enter fullscreen mode Exit fullscreen mode

This creates the RWX StorageClass longhorn-rwx, which uses Longhorn's NFS share-manager pods to support simultaneous multi-pod read-write access. nfsOptions is the main addition that enables the extra NFS mount options RWX volumes need.

3. Apply the StorageClass:

$ kubectl apply -f rwx-storageclass.yaml
Enter fullscreen mode Exit fullscreen mode

4. Verify it's available:

$ kubectl get storageclass
Enter fullscreen mode Exit fullscreen mode

Output:

NAME                                PROVISIONER           RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
longhorn (default)                  driver.longhorn.io    Delete          Immediate           true                   36m
longhorn-prod                      driver.longhorn.io    Retain          Immediate           true                   46s
longhorn-rwx                        driver.longhorn.io    Retain          Immediate           true                   9s
standard                            block.csi.example.com Delete          Immediate           true                   47m
Enter fullscreen mode Exit fullscreen mode

3. Create Persistent Volume Claims (PVCs)

Longhorn dynamically provisions a volume whenever a PVC references one of its StorageClasses.

ReadWriteOnce (RWO) PVC

1. Create the PVC file:

$ nano rwo-pvc.yaml
Enter fullscreen mode Exit fullscreen mode

2. Add the following configuration:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-rwo
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn-prod
  resources:
    requests:
      storage: 10Gi
Enter fullscreen mode Exit fullscreen mode

This creates a PVC test-rwo referencing the longhorn-prod StorageClass, which provisions a 10Gi Longhorn volume once mounted.

3. Apply the PVC:

$ kubectl apply -f rwo-pvc.yaml
Enter fullscreen mode Exit fullscreen mode

4. Verify it's Bound:

$ kubectl get pvc
Enter fullscreen mode Exit fullscreen mode

Output:

NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS    VOLUMEATTRIBUTESCLASS   AGE
test-rwo   Bound    pvc-4026f8d2-873c-429b-8068-208b26a75692   10Gi       RWO            longhorn-prod   <unset>                 4s
Enter fullscreen mode Exit fullscreen mode

5. Verify the underlying PV:

$ kubectl get pv
Enter fullscreen mode Exit fullscreen mode

Output:

NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS    VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-4026f8d2-873c-429b-8068-208b26a75692   10Gi       RWO            Retain           Bound    default/test-rwo   longhorn-prod   <unset>                          8s
Enter fullscreen mode Exit fullscreen mode

Only a single pod can mount and write to this volume at a time.

ReadWriteMany (RWX) PVC

1. Create the PVC file:

$ nano rwx-pvc.yaml
Enter fullscreen mode Exit fullscreen mode

2. Add the following configuration:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: rwx-pvc
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: longhorn-rwx
  resources:
    requests:
      storage: 20Gi
Enter fullscreen mode Exit fullscreen mode

This creates a PVC rwx-pvc referencing longhorn-rwx, provisioning a 20Gi volume that multiple pods can mount simultaneously.

3. Apply the PVC:

$ kubectl apply -f rwx-pvc.yaml
Enter fullscreen mode Exit fullscreen mode

4. Verify both PVCs are Bound:

$ kubectl get pvc
Enter fullscreen mode Exit fullscreen mode

Output:

NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS    VOLUMEATTRIBUTESCLASS   AGE
rwx-pvc    Bound    pvc-509f254e-255d-4aee-bdc1-8c10c6cc49f5   20Gi       RWX            longhorn-rwx    <unset>                 7s
test-rwo   Bound    pvc-4026f8d2-873c-429b-8068-208b26a75692   10Gi       RWO            longhorn-prod   <unset>                 3m10s
Enter fullscreen mode Exit fullscreen mode

5. Verify the PVs:

$ kubectl get pv
Enter fullscreen mode Exit fullscreen mode

Output:

NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM              STORAGECLASS    VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-4026f8d2-873c-429b-8068-208b26a75692   10Gi       RWO            Retain           Bound    default/test-rwo   longhorn-prod   <unset>                          3m31s
pvc-509f254e-255d-4aee-bdc1-8c10c6cc49f5   20Gi       RWX            Retain           Bound    default/rwx-pvc    longhorn-rwx    <unset>                          28s
Enter fullscreen mode Exit fullscreen mode

All pods that mount rwx-pvc can read and write to it at the same time.


4. Test the PVCs

ReadWriteOnce (RWO)

1. Create an Nginx pod that mounts the RWO PVC:

$ nano nginx-rwo-pod.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: v1
kind: Pod
metadata:
  name: nginx-webserver
spec:
  containers:
  - name: rwo-container
    image: nginx:latest
    volumeMounts:
    - name: rwo-volume
      mountPath: /data
  volumes:
  - name: rwo-volume
    persistentVolumeClaim:
      claimName: test-rwo
Enter fullscreen mode Exit fullscreen mode

2. Apply it:

$ kubectl apply -f nginx-rwo-pod.yaml
Enter fullscreen mode Exit fullscreen mode

3. Verify the pod is running:

$ kubectl get pods
Enter fullscreen mode Exit fullscreen mode

Output:

NAME                              READY   STATUS    RESTARTS   AGE
nginx-webserver                   1/1     Running   0          41s
Enter fullscreen mode Exit fullscreen mode

4. Open a shell in the pod:

$ kubectl exec -it nginx-webserver -- /bin/bash
Enter fullscreen mode Exit fullscreen mode
root@nginx-webserver:/# 
Enter fullscreen mode Exit fullscreen mode

5. Write a test page to the mounted volume:

$ echo "<html><body><h1> Hello World! Nginx Writes to the RWO Longhorn Volume </h1></body></html>" > /usr/share/nginx/html/index.html
Enter fullscreen mode Exit fullscreen mode

6. Confirm Nginx serves it:

$ curl 127.0.0.1
Enter fullscreen mode Exit fullscreen mode
<html><body><h1> Hello World! Nginx Writes to the RWO Longhorn Volume </h1></body></html>
Enter fullscreen mode Exit fullscreen mode

Only this one pod can write to the volume at a time — others could mount it read-only, but not write concurrently.

7. Exit the pod:

$ exit
Enter fullscreen mode Exit fullscreen mode

ReadWriteMany (RWX)

RWX is suited to workloads with separate reader/writer processes needing shared storage.

1. Create a backend pod that writes to the shared volume:

$ nano log-backend.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: v1
kind: Pod
metadata:
  name: log-writer
spec:
  containers:
  - name: log-writer-container
    image: busybox:latest
    command: ["/bin/sh", "-c", "while true; do echo $(date) >> /var/log/app.log; sleep 1; done"]
    volumeMounts:
    - name: shared-storage
      mountPath: /var/log
  volumes:
  - name: shared-storage
    persistentVolumeClaim:
      claimName: rwx-pvc
Enter fullscreen mode Exit fullscreen mode

2. Create a second pod that reads from the shared volume:

$ nano log-frontend.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: v1
kind: Pod
metadata:
  name: log-viewer
spec:
  containers:
  - name: log-reader-container
    image: busybox:latest
    command: ["/bin/sh", "-c", "tail -f /var/log/app.log"]
    volumeMounts:
    - name: shared-storage
      mountPath: /var/log
  volumes:
  - name: shared-storage
    persistentVolumeClaim:
      claimName: rwx-pvc
Enter fullscreen mode Exit fullscreen mode

3. Create a second writer pod to prove concurrent writes work:

$ nano updatelog-backend.yaml
Enter fullscreen mode Exit fullscreen mode
apiVersion: v1
kind: Pod
metadata:
  name: updatelog
spec:
  containers:
  - name: log-writer-container
    image: busybox:latest
    command: ["/bin/sh", "-c", "while true; do echo Update from backend pod $(date) >> /var/log/app.log; sleep 1; done"]
    volumeMounts:
    - name: shared-storage
      mountPath: /var/log
  volumes:
  - name: shared-storage
    persistentVolumeClaim:
      claimName: rwx-pvc
Enter fullscreen mode Exit fullscreen mode

4. Apply all three pods:

$ kubectl apply -f log-backend.yaml

$ kubectl apply -f log-frontend.yaml

$ kubectl apply -f updatelog-backend.yaml
Enter fullscreen mode Exit fullscreen mode

5. Verify all three are running:

$ kubectl get pods
Enter fullscreen mode Exit fullscreen mode

Output:

NAME                              READY   STATUS    RESTARTS      AGE
log-viewer                        1/1     Running   1 (37s ago)   59s
log-writer                        1/1     Running   0             64s
updatelog                         1/1     Running   0             51s
Enter fullscreen mode Exit fullscreen mode

6. Confirm both writers are hitting the shared log:

$ kubectl logs log-viewer
Enter fullscreen mode Exit fullscreen mode

Output:

Update from backend pod Tue Apr 30 18:48:19 UTC 2024
Tue Apr 30 18:48:20 UTC 2024
Update from backend pod Tue Apr 30 18:48:20 UTC 2024
Tue Apr 30 18:48:21 UTC 2024
Enter fullscreen mode Exit fullscreen mode

Both log-writer and updatelog write concurrently while log-viewer reads — confirming RWX lets multiple pods share the same volume at once.


5. Configure Longhorn for External Access

Expose the Longhorn UI through an Nginx Ingress controller.

1. Install the Nginx Ingress Controller:

$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.1/deploy/static/provider/cloud/deploy.yaml
Enter fullscreen mode Exit fullscreen mode

2. Verify the controller pod is running:

$ kubectl get pods --all-namespaces -l app.kubernetes.io/name=ingress-nginx
Enter fullscreen mode Exit fullscreen mode

Output:

NAMESPACE       NAME                                        READY   STATUS      RESTARTS   AGE
ingress-nginx   ingress-nginx-admission-create-n2gvf        0/1     Completed   0          98s
ingress-nginx   ingress-nginx-admission-patch-vqfhn         0/1     Completed   0          98s
ingress-nginx   ingress-nginx-controller-57b7568757-ctk5z   1/1     Running     0          99s
Enter fullscreen mode Exit fullscreen mode

3. Wait a few minutes for the controller's LoadBalancer service to get an external IP, then check it:

$ kubectl get services ingress-nginx-controller --namespace=ingress-nginx
Enter fullscreen mode Exit fullscreen mode

Output:

NAME                       TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)                      AGE
ingress-nginx-controller   LoadBalancer   10.106.252.171   203.0.113.5   80:30886/TCP,443:32107/TCP   4m25s
Enter fullscreen mode Exit fullscreen mode

The EXTERNAL-IP is your load balancer's public IP — point a DNS A record at it with your DNS provider. (Some cloud providers require a provider-specific annotation on the Ingress controller's Service before it provisions a load balancer — check your provider's docs if EXTERNAL-IP stays <pending>.)

4. Create an Ingress resource file:

$ nano longhorn-ingress.yaml
Enter fullscreen mode Exit fullscreen mode

5. Add the following, replacing longhorn.example.com with your actual domain:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: longhorn-production44
  namespace: longhorn-system
spec:
  ingressClassName: nginx
  rules:
  - host: longhorn.example.com
    http:
      paths:
      - pathType: Prefix
        path: "/"
        backend:
          service:
            name: longhorn-frontend
            port:
              number: 80
Enter fullscreen mode Exit fullscreen mode

6. Apply the Ingress:

$ kubectl apply -f longhorn-ingress
Enter fullscreen mode Exit fullscreen mode

7. Verify it's configured:

$ kubectl get ingress
Enter fullscreen mode Exit fullscreen mode

Output:

NAME                    CLASS   HOSTS                ADDRESS          PORTS   AGE
longhorn-production   nginx   longhorn.example.com  203.0.113.5      80      2m20s
Enter fullscreen mode Exit fullscreen mode

6. Access the Longhorn UI

1. Open your domain in a browser:

http://longhorn.example.com
Enter fullscreen mode Exit fullscreen mode
  1. Review your cluster's volumes, nodes, and storage summary on the dashboard.

  2. Click Volume in the top navigation to view all Longhorn volumes and confirm your RWO and RWX volumes show a healthy state.

  3. Click any volume to see replica count, snapshots, and backups.

  4. From the volumes list, click Create Volume to provision a new one from the UI — enter the details and use the Access Mode dropdown to pick ReadWriteOnce or ReadWriteMany.

  5. Click OK, then select the new volume and click Attach to assign it to a worker node with enough free space; pick the node from the Node dropdown and click Attach.

  6. Confirm the volume shows a healthy state with Attached To set to your chosen node.


7. Back Up Longhorn Volumes to Object Storage

Longhorn backups work with any S3-compatible object storage for recovery or disaster-recovery volume creation.

  • Get the Hostname, Access Key, and Secret Key for your object storage instance.

1. Create a Secret file for your object storage credentials:

$ nano objectstorage-secret.yaml
Enter fullscreen mode Exit fullscreen mode

2. Add the following, replacing examplekey, examplesecret, and your-s3-endpoint with your real credentials:

apiVersion: v1
kind: Secret
metadata:
  name: longhorn-backup-secret
  namespace: longhorn-system
type: Opaque
stringData:
  AWS_ACCESS_KEY_ID: examplekey
  AWS_ENDPOINTS: https://your-s3-endpoint
  AWS_SECRET_ACCESS_KEY: examplesecret
Enter fullscreen mode Exit fullscreen mode

3. Apply the Secret:

$ kubectl apply -f objectstorage-secret.yaml
Enter fullscreen mode Exit fullscreen mode

4. Verify it exists:

$ kubectl get secrets -n longhorn-system
Enter fullscreen mode Exit fullscreen mode

Output:

NAME                      TYPE                DATA   AGE
longhorn-backup-secret    Opaque              3      35s
Enter fullscreen mode Exit fullscreen mode

5. In the Longhorn UI, go to Setting → General, scroll to Backup, and set the Backup Target to your bucket's S3 URI:

s3://example-bucket@your-s3-endpoint/
Enter fullscreen mode Exit fullscreen mode

6. Enter your Secret's name (longhorn-backup-secret) in the Backup Target Credential Secret field, then press Enter to save.

  1. Click Volume, select a target volume (e.g. test-volume), and click Create Backup.

  2. Add a label if desired (e.g. longhorn-backup / latest-backup), then wait for the backup to complete.

  3. Click Backup in the main navigation to confirm the new backup is listed.

  4. Check your object storage bucket for a new backupstore directory containing the backup objects.

Once backups are running, use Recurring Job in the Longhorn UI to automate them going forward.


Next Steps

  • Set up a Longhorn Recurring Job to automate volume backups on a schedule
  • Deploy stateful workloads (databases, queues) using the RWO or RWX StorageClasses you created
  • Monitor volume and replica health from the Longhorn UI dashboard
  • Test a disaster-recovery restore from your object storage backups to confirm your recovery process works

For the full guide with additional tips, visit the original article on Vultr Docs.

Top comments (0)