# 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, following distributed Kubernetes storage deployment practices documented in [**Vultr Docs**](https://docs.vultr.com/how-to-install-longhorn-on-kubernetes).

> **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:**

```console
wget https://github.com/longhorn/longhorn/releases/download/v1.6.1/longhorn.yaml
```

This downloads Longhorn `v1.6.1` — check the [GitHub repository](https://github.com/longhorn/longhorn/) for the latest version before deploying.

**2\. Verify the file downloaded:**

```console
ls
```

Output:

```plaintext
longhorn.yaml
```

**3\. Deploy Longhorn to your cluster:**

```console
kubectl apply -f longhorn.yaml
```

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:**

```console
kubectl get all -n longhorn-system
```

Example output:

```plaintext
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
```

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

```console
kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.6.1/deploy/prerequisite/longhorn-nfs-installation.yaml
```

**6\. Confirm the NFS installation pods are running:**

```console
kubectl get pods | grep longhorn-nfs-installation
```

Output:

```plaintext
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
```

* * *

## 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:**

```console
nano rwo-storageclass.yaml
```

**2\. Add the following configuration:**

```yaml
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
```

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

*   `provisioner` — `driver.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.
    
*   `reclaimPolicy` — `Delete` or `Retain` behavior when the bound PVC/PV is removed.
    

**3\. Apply the StorageClass:**

```console
kubectl apply -f rwo-storageclass.yaml
```

**4\. Verify it's available:**

```console
kubectl get storageclass
```

Output:

```plaintext
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
```

(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:**

```console
nano rwx-storageclass.yaml
```

**2\. Add the following configuration:**

```yaml
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  
```

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:**

```console
kubectl apply -f rwx-storageclass.yaml
```

**4\. Verify it's available:**

```console
kubectl get storageclass
```

Output:

```plaintext
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
```

* * *

## 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:**

```console
nano rwo-pvc.yaml
```

**2\. Add the following configuration:**

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-rwo
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn-prod
  resources:
    requests:
      storage: 10Gi
```

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

**3\. Apply the PVC:**

```console
kubectl apply -f rwo-pvc.yaml
```

**4\. Verify it's** `Bound`**:**

```console
kubectl get pvc
```

Output:

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

**5\. Verify the underlying PV:**

```console
kubectl get pv
```

Output:

```plaintext
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
```

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

### ReadWriteMany (RWX) PVC

**1\. Create the PVC file:**

```console
nano rwx-pvc.yaml
```

**2\. Add the following configuration:**

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: rwx-pvc
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: longhorn-rwx
  resources:
    requests:
      storage: 20Gi
```

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

**3\. Apply the PVC:**

```console
kubectl apply -f rwx-pvc.yaml
```

**4\. Verify both PVCs are** `Bound`**:**

```console
kubectl get pvc
```

Output:

```plaintext
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
```

**5\. Verify the PVs:**

```console
kubectl get pv
```

Output:

```plaintext
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
```

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:**

```console
nano nginx-rwo-pod.yaml
```

```yaml
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
```

**2\. Apply it:**

```console
kubectl apply -f nginx-rwo-pod.yaml
```

**3\. Verify the pod is running:**

```console
kubectl get pods
```

Output:

```plaintext
NAME                              READY   STATUS    RESTARTS   AGE
nginx-webserver                   1/1     Running   0          41s
```

**4\. Open a shell in the pod:**

```console
kubectl exec -it nginx-webserver -- /bin/bash
```

```console
root@nginx-webserver:/# 
```

**5\. Write a test page to the mounted volume:**

```console
echo "<html><body><h1> Hello World! Nginx Writes to the RWO Longhorn Volume </h1></body></html>" > /usr/share/nginx/html/index.html
```

**6\. Confirm Nginx serves it:**

```console
curl 127.0.0.1
```

```html
<html><body><h1> Hello World! Nginx Writes to the RWO Longhorn Volume </h1></body></html>
```

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:**

```console
exit
```

### 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:**

```console
nano log-backend.yaml
```

```yaml
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
```

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

```console
nano log-frontend.yaml
```

```yaml
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
```

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

```console
nano updatelog-backend.yaml
```

```yaml
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
```

**4\. Apply all three pods:**

```console
kubectl apply -f log-backend.yaml

kubectl apply -f log-frontend.yaml

kubectl apply -f updatelog-backend.yaml
```

**5\. Verify all three are running:**

```console
kubectl get pods
```

Output:

```plaintext
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
```

**6\. Confirm both writers are hitting the shared log:**

```console
kubectl logs log-viewer
```

Output:

```plaintext
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
```

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:**

```console
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.1/deploy/static/provider/cloud/deploy.yaml
```

**2\. Verify the controller pod is running:**

```console
kubectl get pods --all-namespaces -l app.kubernetes.io/name=ingress-nginx
```

Output:

```plaintext
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
```

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

```console
kubectl get services ingress-nginx-controller --namespace=ingress-nginx
```

Output:

```plaintext
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
```

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:**

```console
nano longhorn-ingress.yaml
```

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

```yaml
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
```

**6\. Apply the Ingress:**

```console
kubectl apply -f longhorn-ingress
```

**7\. Verify it's configured:**

```console
kubectl get ingress
```

Output:

```plaintext
NAME                    CLASS   HOSTS                ADDRESS          PORTS   AGE
longhorn-production   nginx   longhorn.example.com  203.0.113.5      80      2m20s
```

* * *

## 6\. Access the Longhorn UI

**1\. Open your domain in a browser:**

```plaintext
http://longhorn.example.com
```

2.  Review your cluster's volumes, nodes, and storage summary on the dashboard.
    
3.  Click **Volume** in the top navigation to view all Longhorn volumes and confirm your RWO and RWX volumes show a healthy state.
    
4.  Click any volume to see replica count, snapshots, and backups.
    
5.  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.
    
6.  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**.
    
7.  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:**

```console
nano objectstorage-secret.yaml
```

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

```yaml
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
```

**3\. Apply the Secret:**

```console
kubectl apply -f objectstorage-secret.yaml
```

**4\. Verify it exists:**

```console
kubectl get secrets -n longhorn-system
```

Output:

```plaintext
NAME                      TYPE                DATA   AGE
longhorn-backup-secret    Opaque              3      35s
```

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

```plaintext
s3://example-bucket@your-s3-endpoint/
```

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

7.  Click **Volume**, select a target volume (e.g. `test-volume`), and click **Create Backup**.
    
8.  Add a label if desired (e.g. `longhorn-backup` / `latest-backup`), then wait for the backup to complete.
    
9.  Click **Backup** in the main navigation to confirm the new backup is listed.
    
10.  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**](https://docs.vultr.com/how-to-install-longhorn-on-kubernetes).
