Skip to main content

Kubernetes: Longhorn Persistence

Kubernetes - This article is part of a series.
Part 4: This Article

Using Longhorn for cloud native persistent volumes
#

Longhorn provides cloud native, persistent volume provisioning with replication and easy backups. Perfect for a homelab, it can be easily deployed to our k3s cluster using the official Helm chart.

Deploying Longhorn
#

Helm chart
#

First, we add modules to import the chart and set the namespace:

Note

The namespace must be set to longhorn-system for the deployment to work

{
  self,
  inputs,
  ...
}:
{
  flake.modules.nixos.longhorn-charts =
    {
      config,
      lib,
      pkgs,
      ...
    }:
    let
      longhornChart = {
        name = "longhorn";
        repo = "https://charts.longhorn.io";
        version = "1.10.1";
        hash = "sha256-qHHTl+Gc8yQ5SavUH9KUhp9cLEkAFPKecYZqJDPsf7k=";
      };
    in
    {
      config = lib.mkIf config.longhorn.enable {
        services.k3s.autoDeployCharts = {
          longhorn = longhornChart // {
            targetNamespace = "longhorn-system";
            createNamespace = true;
          };
        };
      };
    };
}

Images
#

Then we preload each image used by the Helm chart:

{
  self,
  inputs,
  ...
}:
{
  flake.modules.nixos.longhorn-images =
    {
      config,
      lib,
      pkgs,
      ...
    }:
    let
      csiAttacherImage = pkgs.dockerTools.pullImage {
        imageName = "longhornio/csi-attacher";
        imageDigest = "sha256:af29125d83075b95894e95862dde03908b1970858f1d6399917305b92d2713a6";
        sha256 = "sha256-fSEQ6iUnr7qDdPI59lZIb3mExbRM+6nTV9HY9dXOYp8=";
        finalImageTag = "v4.10.0-20251030";
        arch = "amd64";
      };
      csiNodeDriverRegistrarImage = pkgs.dockerTools.pullImage {
        imageName = "longhornio/csi-node-driver-registrar";
        imageDigest = "sha256:cc125aa681e8ff41ef97bf72b8c16cafffa84f8493ecbcf606e5463f90e9cbd3";
        sha256 = "sha256-FcgRdmuQc/E4tOwJfIy98KS682EC1QnXAVIHiFDz0a0=";
        finalImageTag = "v2.15.0-20251030";
        arch = "amd64";
      };
      csiProvisionerImage = pkgs.dockerTools.pullImage {
        imageName = "longhornio/csi-provisioner";
        imageDigest = "sha256:fe31c68584e80a2af9ae14e0682b3cea076218a0756ae7add0a421c21486a4d0";
        sha256 = "sha256-vZ1ifQSUC0uswdGjueNU4HciIULTWvriNaO6gfeoDJ4=";
        finalImageTag = "v5.3.0-20251030";
        arch = "amd64";
      };
      csiResizerImage = pkgs.dockerTools.pullImage {
        imageName = "longhornio/csi-resizer";
        imageDigest = "sha256:748b61cb91ba4cc4bc35ba38bdac73c130602262458f388b0116eb96a4690e94";
        sha256 = "sha256-SxgDbSseKIRfN3tZufyvMXSxHy9EJf6x6jmZttieLsc=";
        finalImageTag = "v1.14.0-20251030";
        arch = "amd64";
      };
      csiSnapshotterImage = pkgs.dockerTools.pullImage {
        imageName = "longhornio/csi-snapshotter";
        imageDigest = "sha256:2cc2f9e653c2b397e8eefffbfb4ccf8eabc5f7d84e1f875d42608484a66c307f";
        sha256 = "sha256-N7zYMfgAaOC6f0g/s5+WyL/UrZKRc3LBXLC2DP1lxdQ=";
        finalImageTag = "v8.4.0-20251030";
        arch = "amd64";
      };
      csiLivenessProbeImage = pkgs.dockerTools.pullImage {
        imageName = "longhornio/livenessprobe";
        imageDigest = "sha256:7aed397ffdcb125374fa4dd44d95251598e9e97940bc5b84187b3cd3ffd655fd";
        sha256 = "sha256-LMBH+hmOhRVoRyKneWb7kWgzEy3rdo/9aqeuhUreW9g=";
        finalImageTag = "v2.17.0-20251030";
        arch = "amd64";
      };
      longhornEngineImage = pkgs.dockerTools.pullImage {
        imageName = "longhornio/longhorn-engine";
        imageDigest = "sha256:b6b30ace865932a686afde56757213d6c86834645443f3af1528a93b7ddf52f4";
        sha256 = "sha256-ussctYityuRM7DmqtKKGorj22SkcVHvtc3OP03A3xF8=";
        finalImageTag = "v1.10.1";
        arch = "amd64";
      };
      longhornInstanceManagerImage = pkgs.dockerTools.pullImage {
        imageName = "longhornio/longhorn-instance-manager";
        imageDigest = "sha256:84e0a5c1d67599a445f5b4fa853152ff53f6b1bd42a7cf7c01f4152cf60782af";
        sha256 = "sha256-+6DObQfiNghYFuMfERjjToOOkN1ZOGGTt4DWBF9c4GU=";
        finalImageTag = "v1.10.1";
        arch = "amd64";
      };
      longhornManagerImage = pkgs.dockerTools.pullImage {
        imageName = "longhornio/longhorn-manager";
        imageDigest = "sha256:afda26c16e7ab106f94dbc11da1bc91f410487d2e66609ebd126f0d908f7243a";
        sha256 = "sha256-BP01Yxeaqq256XNXgctc2NWSdONVNkmJlS+e6izTCIU=";
        finalImageTag = "v1.10.1";
        arch = "amd64";
      };
      longhornShareManagerImage = pkgs.dockerTools.pullImage {
        imageName = "longhornio/longhorn-share-manager";
        imageDigest = "sha256:1edc95ae8f9e9699f9b082bf0eac82b338b2f120462424201957cb6287b2e3e9";
        sha256 = "sha256-3CJxq42YuXusgcWdLuzgTrsAgivlRM5M+5WsxpHrTDU=";
        finalImageTag = "v1.10.1";
        arch = "amd64";
      };
      longhornUiImage = pkgs.dockerTools.pullImage {
        imageName = "longhornio/longhorn-ui";
        imageDigest = "sha256:62fd171f4fbed01ebb51653674c68ea1c531aa562dab23cb029033dffd6bccc6";
        sha256 = "sha256-zzAyKsmAQb/aBE157lg0NxCp7jzHmtUwZI1cg0CS/rs=";
        finalImageTag = "v1.10.1";
        arch = "amd64";
      };
    in
    {
      config = lib.mkIf config.longhorn.enable {
        services.k3s = {
          images = [
            csiAttacherImage
            csiNodeDriverRegistrarImage
            csiProvisionerImage
            csiResizerImage
            csiSnapshotterImage
            csiLivenessProbeImage
            longhornEngineImage
            longhornInstanceManagerImage
            longhornManagerImage
            longhornShareManagerImage
            longhornUiImage
          ];
          autoDeployCharts.longhorn.values = {
            image = {
              csi = {
                attacher = {
                  repository = csiAttacherImage.imageName;
                  tag = csiAttacherImage.imageTag;
                };
                nodeDriverRegistrar = {
                  repository = csiNodeDriverRegistrarImage.imageName;
                  tag = csiNodeDriverRegistrarImage.imageTag;
                };
                provisioner = {
                  repository = csiProvisionerImage.imageName;
                  tag = csiProvisionerImage.imageTag;
                };
                resizer = {
                  repository = csiResizerImage.imageName;
                  tag = csiResizerImage.imageTag;
                };
                snapshotter = {
                  repository = csiSnapshotterImage.imageName;
                  tag = csiSnapshotterImage.imageTag;
                };
                livenessProbe = {
                  repository = csiLivenessProbeImage.imageName;
                  tag = csiLivenessProbeImage.imageTag;
                };
              };
              longhorn = {
                engine = {
                  repository = longhornEngineImage.imageName;
                  tag = longhornEngineImage.imageTag;
                };
                instanceManager = {
                  repository = longhornInstanceManagerImage.imageName;
                  tag = longhornInstanceManagerImage.imageTag;
                };
                manager = {
                  repository = longhornManagerImage.imageName;
                  tag = longhornManagerImage.imageTag;
                };
                shareManager = {
                  repository = longhornShareManagerImage.imageName;
                  tag = longhornShareManagerImage.imageTag;
                };
                ui = {
                  repository = longhornUiImage.imageName;
                  tag = longhornUiImage.imageTag;
                };
              };
            };
          };
        };
      };
    };
}

Configuring Longhorn for a single node configuration
#

As Longhorn defaults to a Highly Available configuration with three replicas of each service and persistent volume we must add a module to properly set each replication count to one. Additionally, we must update the reclaim policy so that volumes are not recreated upon system reboots and lower the minimum required storage required to 10% to make the most out of limited system storage:

{
  self,
  inputs,
  ...
}:
{
  flake.modules.nixos.longhorn-settings =
    {
      config,
      lib,
      pkgs,
      ...
    }:
    {
      config = lib.mkIf config.longhorn.enable {
        services.k3s.autoDeployCharts.longhorn.values = {
          longhornUI.replicas = 1;
          defaultSettings = {
            defaultReplicaCount = 1;
            storageMinimalAvailablePercentage = 10;
            storageReservedPercentageForDefaultDisk = 10;
          };
          csi = {
            attacherReplicaCount = 1;
            provisionerReplicaCount = 1;
            resizerReplicaCount = 1;
            snapshotterReplicaCount = 1;
          };
          persistence = {
            defaultClassReplicaCount = 1;
            reclaimPolicy = "Retain";
          };
        };
      };
    };
}

Adding persistent volume claims
#

Once Longhorn is deployed, it should automatically be set as the default storage class in k3s. This means persistent volumes can be defined in manifests as such:

{
  self,
  inputs,
  ...
}:
{
  flake.modules.nixos.immich-persistence =
    {
      config,
      lib,
      pkgs,
      ...
    }:
    {
      config = lib.mkIf config.immich.enable {
        services.k3s.autoDeployCharts.immich = {
          values.immich.persistence.library.existingClaim = "immich-pvc";
          extraDeploy = [
            {
              apiVersion = "v1";
              kind = "PersistentVolumeClaim";
              metadata = {
                name = "immich-pvc";
                namespace = "immich";
              };
              spec = {
                accessModes = [ "ReadWriteOnce" ];
                resources = {
                  requests = {
                    storage = "25Gi";
                  };
                };
              };
            }
          ];
        };
      };
    };
}

Accessing the dashboard
#

Longhorn provides and easy to use web frontend. Read the following sections on MetalLB and the Netbird Operator to see how to expose this service to the local network and private vpn respectively.

Longhorn Dashboard

Kubernetes - This article is part of a series.
Part 4: This Article