Kubernetes Storage in 2026: PersistentVolumes, StorageClasses and StatefulSets Without the Fear
Kubernetes

Kubernetes Storage in 2026: PersistentVolumes, StorageClasses and StatefulSets Without the Fear

By DevOps Enginerβ€’β€’10 min read

Kubernetes Storage in 2026: PersistentVolumes, StorageClasses and StatefulSets Without the Fear

> TL;DR: Kubernetes storage confuses people because it splits one idea β€” "my pod needs a disk" β€” into three objects. A PersistentVolumeClaim (PVC) is the pod's request: "I need 10Gi, writable by one node." A PersistentVolume (PV) is the actual disk that satisfies it. A StorageClass is the recipe that creates those disks on demand, with a chosen type, reclaim behaviour and expansion policy. Stateless apps should stay on Deployments with no volumes at all. Apps that genuinely own data β€” databases, queues, anything whose identity is its disk β€” belong on StatefulSets, which give each replica a stable name, a stable network identity and its own claim that survives rescheduling. Most storage incidents come from three places: the wrong reclaim policy deleting data, a volume bound to one availability zone while the pod reschedules into another, and nobody testing a restore until the day it matters.

Pods are cattle; disks are pets. Everything awkward about Kubernetes storage lives in that sentence. This guide walks the moving parts in the order a request flows through them, then covers StatefulSets, expansion, snapshots, and the failure modes that actually bite.

The three objects, in the order they meet

When a pod declares it needs storage, this is the chain:

  1. PersistentVolumeClaim β€” the request, written by you (or generated by a StatefulSet). It asks for a size, an access mode, and optionally a StorageClass.
  2. StorageClass β€” the template. It names the provisioner (your cloud's disk service, a CSI driver, a local provisioner), the parameters (disk type, IOPS tier), the reclaim policy, and whether volumes can be expanded later.
  3. PersistentVolume β€” the reality. With dynamic provisioning (the normal case on any cloud cluster), the StorageClass's provisioner creates a real disk and a PV object representing it, bound one-to-one to your claim.

You almost never create PVs by hand anymore. You write PVCs, you choose (or set a default) StorageClass, and provisioning does the rest. If a claim sits in Pending, the conversation between those three objects broke somewhere β€” usually the class doesn't exist, the provisioner can't create disks in that zone, or quota ran out. kubectl describe pvc prints the events that tell you which.

Access modes: the line that decides your architecture

Every claim declares how the volume may be mounted, and this choice constrains everything downstream:

  • ReadWriteOnce (RWO) β€” one node at a time. The default for cloud block disks (EBS, Azure Disk, GCE PD). Fine for a database with one writer; it also means a rolling update can deadlock if the new pod lands on a different node while the old pod still holds the volume β€” the classic multi-attach error.
  • ReadWriteMany (RWX) β€” many nodes at once. Requires a shared filesystem (NFS, EFS, Azure Files, Filestore). Needed when several pods across nodes must see the same files; you pay for it in performance characteristics and complexity.
  • ReadWriteOncePod (RWOP) β€” one *pod*, not just one node (Kubernetes 1.29+ for CSI volumes). The stricter guarantee single-writer workloads actually wanted.

Read the mode before you design around a volume. Half of all "Kubernetes storage is broken" stories are a workload assuming RWX semantics on an RWO disk.

StorageClasses: the decisions hiding in the defaults

One cluster usually offers several classes β€” a fast SSD class, a balanced class, maybe an HDD class. The fields that matter most are the quiet ones:

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-retain
provisioner: ebs.csi.aws.com        # example: AWS EBS CSI driver
parameters:
  type: gp3
reclaimPolicy: Retain               # not Delete β€” see below
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
  • reclaimPolicy decides what happens to the disk when the PVC is deleted. Delete destroys the underlying volume β€” convenient for throwaway environments, catastrophic for a production database whose claim gets deleted by a namespace cleanup or a careless Helm uninstall. For anything whose data matters: Retain. Retain leaves the PV in a Released state; the data survives until a human deliberately deals with it.
  • volumeBindingMode: WaitForFirstConsumer delays provisioning until a pod actually schedules. Without it, a disk can be created in zone A before the scheduler places the pod in zone B β€” and a zonal disk can never attach across zones, so the pod is stuck forever. This single field prevents one of the most common storage dead-ends on multi-zone clusters.
  • allowVolumeExpansion: true is only settable at class creation in effect β€” you cannot retroactively make a non-expandable class expandable for existing volumes in most setups. Set it now, thank yourself later.

Check what your cluster defaults to before writing a single claim:

bash
kubectl get storageclass
kubectl describe pvc <claim> -n <namespace>
kubectl get pv

StatefulSets: when the pod's identity IS the disk

Deployments treat replicas as interchangeable β€” perfect for stateless services, wrong for a database cluster where db-0, db-1 and db-2 are different members with different data. StatefulSets exist for those workloads and give three guarantees Deployments don't:

  • Stable identity: pods are named name-0, name-1, … and keep that name across restarts, with a matching stable DNS entry per pod via a headless Service.
  • Per-replica storage: a volumeClaimTemplates block generates one PVC per replica (data-db-0, data-db-1, …). When a pod is deleted and recreated, it reattaches to *its* claim β€” the data follows the identity, not the node.
  • Ordered behaviour: replicas start and stop in order, which clustered systems that elect leaders and replicate in sequence genuinely need.
yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
spec:
  serviceName: db-headless
  replicas: 3
  selector:
    matchLabels: { app: db }
  template:
    metadata:
      labels: { app: db }
    spec:
      containers:
        - name: db
          image: postgres:16
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-retain
        resources:
          requests:
            storage: 20Gi

Two operational truths about StatefulSet claims. First, scaling down does not delete the claims β€” scale from 3 back to 5 later and the old replicas reattach to their existing data. That is deliberate and usually what you want; it also means forgotten claims quietly keep disks (and bills) alive, which is why storage belongs in the cost optimization review. Second, deleting a StatefulSet leaves its PVCs behind too β€” cleanup is a separate, explicit act. Annoying on a tidy-up day; a lifesaver on a bad day.

And the honest question to ask first: does this workload need to be stateful *in Kubernetes at all*? A managed database service outside the cluster is often the right answer for small teams. Run state in-cluster when you have a real reason β€” latency, cost at scale, data locality β€” not by default. If you do run it, the deployment strategies guide covers how updates roll for these workloads, and it is not "like a Deployment."

Growing and protecting volumes

Expansion is the happy path, when it was prepared: with allowVolumeExpansion: true on the class, you edit the PVC's requested size upward and the provisioner grows the disk; most filesystems expand online once the pod restarts or remounts. Shrinking is not supported β€” a smaller disk means a new volume and a data migration, full stop.

Snapshots are the backup primitive: a VolumeSnapshot asks the CSI driver for a point-in-time copy, and a new PVC can be created *from* a snapshot to restore or clone. Combined with a schedule (via your backup tooling or a CronJob-driven process), this is how in-cluster state gets real backups. Two warnings from experience-shaped convention:

  • A snapshot is not a backup strategy until a restore has been tested. Untested restores fail in creative ways: wrong class, wrong zone, permissions, application-level inconsistency.
  • For databases, storage-level snapshots are crash-consistent at best unless the application quiesces writes. Where correctness matters, pair snapshots with the database's own backup tooling.

Security applies to disks too: volume contents inherit your RBAC and Pod Security posture β€” whoever can create a PVC from a snapshot can read the data in it, so snapshot permissions deserve the same least-privilege care as the volumes themselves.

Common mistakes

  1. reclaimPolicy: Delete on data that matters. The claim gets deleted β€” by a person, a script, a namespace teardown β€” and the disk goes with it. Retain for production state, always.
  2. No WaitForFirstConsumer on multi-zone clusters. Disks provisioned in the wrong zone; pods Pending forever with attach errors that look like scheduler problems.
  3. Assuming RWX from an RWO disk. Multi-attach errors during rollouts, or worse, silent assumptions in the app design that only surface under failover.
  4. Forgetting StatefulSet claims on cleanup. Orphaned PVs holding real disks for months β€” visible in the bill, invisible in kubectl get pods.
  5. Snapshots nobody has restored from. A backup you have never tested is a hypothesis. Schedule a restore drill like you schedule the backup.
  6. Running stateful workloads on Deployments with a shared claim. Losing stable identity and per-replica storage is how databases end up with two writers and one corrupted dataset.

FAQ

Should my web app have a PersistentVolume? Almost never. If the app is stateless, uploads and user files belong in object storage (S3/GCS/Azure Blob) or a managed service, not a pod-local disk. PVCs are for workloads whose data is the workload β€” databases, brokers, search indexes.

What happens to my data if the pod is deleted? With a PVC, nothing β€” the volume outlives the pod and reattaches to its replacement. With no volume (emptyDir or the container filesystem), everything is gone with the pod. That distinction is the entire subject of this article.

Can I move a volume to another cluster? Not by copying YAML. The path is snapshot/restore (CSI snapshots, or backup tools built on them) or application-level replication/export. Plan migrations around the data tooling, not the manifests.

Why is my PVC stuck in Pending? Read its events: kubectl describe pvc. The usual causes are a StorageClass that doesn't exist, a provisioner that can't create volumes in the needed zone, exhausted quota, or WaitForFirstConsumer waiting (correctly) for a pod that itself can't schedule β€” in which case the pod's events, covered in the troubleshooting guide, hold the real reason.

Conclusion

Kubernetes storage stops being scary when you keep the chain straight: claims ask, classes define, volumes provide β€” and StatefulSets bind identity to data for the workloads that need it. Choose the access mode honestly, set Retain and WaitForFirstConsumer before they save you, grow volumes instead of replacing them, and test restores before you need them. Disks are pets; treat them with the deliberate care pets require, and they will stop biting.

*The free Kubernetes Production Checklist (via the newsletter on the homepage) includes the storage pre-flight items β€” reclaim policies, backup tests and disruption budgets β€” alongside the rest of the production readiness checks.*

Tags

#Kubernetes#Storage#StatefulSets#Tutorial

Related Articles