Kubernetes Persistent Storage: PVCs, Storage Classes, and Operators That Actually Work

Kubernetes Persistent Storage: PVCs, Storage Classes, and Operators That Actually Work

Kubernetes was designed with stateless applications in mind. Pods are ephemeral. Deployments replace pods. Everything is immutable infrastructure. And then you need to run a database, and suddenly “just restart the pod” doesn’t work like it does for your API server.

Persistent storage in Kubernetes has matured significantly. The primitives are solid, the CSI driver ecosystem is extensive, and there are production-grade distributed storage solutions that work well in both cloud and on-premises environments. Understanding the stack is essential for running anything stateful in Kubernetes.

The Storage Abstraction Stack

Kubernetes storage involves several layers:

Application
    ↓
PersistentVolumeClaim (PVC) — what the pod requests
    ↓
PersistentVolume (PV) — the actual storage resource
    ↓
StorageClass — defines how PVs are provisioned
    ↓
CSI Driver — communicates with the underlying storage system
    ↓
Storage Backend (Longhorn, Ceph, cloud EBS, NFS, etc.)

PersistentVolumeClaim

The PVC is what your pod actually references. It’s a request for storage with specific characteristics:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-data
  namespace: database
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: longhorn
  resources:
    requests:
      storage: 50Gi

The pod references the PVC by name:

volumes:
  - name: postgres-data
    persistentVolumeClaim:
      claimName: postgres-data
containers:
  - name: postgres
    volumeMounts:
      - name: postgres-data
        mountPath: /var/lib/postgresql/data

Access Modes

ReadWriteOnce (RWO): Can be mounted by a single node in read/write mode. Most block storage is RWO. Appropriate for databases.

ReadOnlyMany (ROX): Can be mounted by many nodes in read-only mode. Good for static content.

ReadWriteMany (RWX): Can be mounted by many nodes in read/write mode. Requires shared storage (NFS, CephFS, etc.). Needed for applications with multiple replicas that share a volume.

ReadWriteOncePod (RWOP): Kubernetes 1.22+. Can only be mounted by a single pod (not just node). More restrictive than RWO.

Choose the most restrictive access mode that works for your use case. Most databases need RWO; only choose RWX when you genuinely need shared concurrent access.

StorageClasses

StorageClasses define the “class” of storage—what type it is, how it’s provisioned, and what capabilities it has:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: longhorn-fast
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: driver.longhorn.io
parameters:
  numberOfReplicas: "3"
  staleReplicaTimeout: "2880"
  diskSelector: "ssd"  # Only use SSDs for this class
reclaimPolicy: Retain  # ALWAYS use Retain in production
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

The reclaimPolicy: Retain is critical. The alternatives are Delete (the volume is deleted when the PVC is deleted) and Recycle (deprecated). In production, you always want Retain—if someone accidentally deletes a PVC, you want the underlying data to still exist until you explicitly decide to delete it.

Longhorn: The Homelab and SMB Production Choice

Longhorn is a CNCF project that provides distributed block storage built natively for Kubernetes. It runs entirely within your Kubernetes cluster—no external storage system required.

How it works: Longhorn carves space from local node disks, replicates data across multiple nodes, and presents it as PersistentVolumes. Replicas are stored on different nodes, so the failure of a node doesn’t cause data loss.

Why it’s great for homelab:

  • No external storage required—uses your nodes’ local disks
  • Web UI for volume management
  • Snapshots and backup to S3-compatible storage
  • Volume expansion online
  • CNCF-graduated, actively maintained by SUSE
# Longhorn HelmRelease
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: longhorn
  namespace: longhorn-system
spec:
  chart:
    spec:
      chart: longhorn
      version: ">=1.6.0 <2.0.0"
      sourceRef:
        kind: HelmRepository
        name: longhorn
        namespace: flux-system
  values:
    defaultSettings:
      defaultReplicaCount: 3
      defaultDataLocality: best-effort
      backupTarget: "s3://my-backup-bucket@us-east-1/"
      backupTargetCredentialSecret: longhorn-backup-credentials
    ingress:
      enabled: true
      ingressClassName: nginx
      host: longhorn.internal.example.com

Rook-Ceph: Enterprise-Grade Distributed Storage

For larger clusters or enterprise requirements, Rook is a Kubernetes operator for Ceph—the most capable open-source distributed storage system.

Ceph provides:

  • Block storage (RBD) — for databases, comparable to Longhorn
  • Object storage (RGW) — S3-compatible API
  • Filesystem (CephFS) — ReadWriteMany support
# Ceph cluster definition
apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
  name: rook-ceph
  namespace: rook-ceph
spec:
  cephVersion:
    image: quay.io/ceph/ceph:v18.2
  dataDirHostPath: /var/lib/rook
  mon:
    count: 3
    allowMultiplePerNode: false
  mgr:
    count: 2
  storage:
    useAllNodes: true
    useAllDevices: false
    deviceFilter: "^sd[b-z]"  # Use devices sdb through sdz
  resources:
    osd:
      requests:
        cpu: "500m"
        memory: "2Gi"
      limits:
        memory: "4Gi"

Rook-Ceph is more complex to operate than Longhorn but more capable. Consider it if you need:

  • ReadWriteMany volumes at scale
  • S3-compatible object storage internal to your cluster
  • Multiple petabytes of storage
  • Enterprise data protection features

StatefulSets: Managing Stateful Applications

When running databases in Kubernetes, use StatefulSets instead of Deployments:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: database
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    spec:
      containers:
        - name: postgres
          image: postgres:16-alpine
          env:
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-credentials
                  key: password
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          volumeMounts:
            - name: postgres-storage
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: postgres-storage
        annotations:
          # Ensure PVC is retained even if StatefulSet is deleted
          helm.sh/resource-policy: keep
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: longhorn
        resources:
          requests:
            storage: 50Gi

StatefulSets give each pod a stable network identity and stable persistent storage. Pod postgres-0 always gets PVC postgres-storage-postgres-0. This is essential for databases where each instance needs to know its identity.

Backup Strategy

Storing data in Kubernetes doesn’t excuse you from backup. Longhorn and Rook both have backup capabilities:

# Longhorn recurring backup job
apiVersion: longhorn.io/v1beta2
kind: RecurringJob
metadata:
  name: backup-daily
  namespace: longhorn-system
spec:
  cron: "0 2 * * *"
  task: "backup"
  groups:
    - default
  retain: 14
  concurrency: 2
  labels:
    backup: "daily"

Backups go to S3-compatible object storage. Test your restores regularly—a backup you haven’t tested is just data you haven’t lost yet.

The PVC Retention Rule

All PVCs must be retained if a HelmRelease is deleted.

Implement this in two ways:

  1. StorageClass with reclaimPolicy: Retain (as shown above)
  2. Helm annotation on PVC templates:
annotations:
  helm.sh/resource-policy: keep

Both together provides the strongest protection. A Helm uninstall won’t delete the PVC (due to the annotation), and even if the PVC is manually deleted, the PV won’t be deleted (due to the Retain policy). You’d have to explicitly delete both the PVC and then the PV to lose data.

This is the right default for production data. Always retain; explicitly delete when you’re sure you don’t need the data.

Share this post: LinkedIn Reddit WhatsApp Mastodon
Jesse Borden

Jesse Borden

Software Engineer with an interest in hands on learning

I have several years of professional Information Technology (IT) experience leading staff and projects within the Department of War (DOW). I have managed Service Desk, Web Application Development, and System Administration teams. My two greatest passions are learning and conti...