Storage is the part of Kubernetes where the abstraction stops hiding the hardware. A claim that will not bind, a replica stuck Pending, a rolling update that waits forever: each traces back to one property of the disk underneath. Getting the vocabulary and the access modes straight up front saves the rebuild that otherwise follows the first database.

Stateless workloads make Kubernetes look easy. The first database moves in and suddenly you are reading about access modes, reclaim policies and why a pod will not schedule, because storage is the one area where the abstraction leaks and the underlying disk asserts itself.

The Three Objects, and What Each Owns

The vocabulary confuses people because three objects describe what feels like one thing.

PersistentVolume (PV) is a piece of storage that exists. Cluster-scoped, created either by an administrator or automatically by a provisioner.

PersistentVolumeClaim (PVC) is a request for storage, made in a namespace by a workload. It asks for a size, an access mode and optionally a StorageClass.

StorageClass is the recipe for creating PVs on demand. It names a provisioner and its parameters, and it is what turns a claim into an actual disk without anybody filing a ticket.

A pod references a PVC, never a PV. The binding between claim and volume is the cluster's job, and once bound it stays bound: a PVC does not migrate to a different PV because a pod moved.

kubectl get pvc -A
kubectl get pv
kubectl get storageclass
kubectl describe pvc my-data   # the events explain a Pending claim

Access Modes Decide Your Architecture

This is the single most consequential detail and the one most often skipped.

ModeWhat it permitsTypical backing
ReadWriteOnceMounted read-write by pods on one node at a timeBlock storage, Ceph RBD, cloud disks, local disks
ReadOnlyManyMounted read-only by pods on many nodesShared filesystems, pre-populated datasets
ReadWriteManyMounted read-write by pods on many nodesNFS, CephFS, other network filesystems
ReadWriteOncePodMounted read-write by exactly one podBlock storage, where strict exclusivity matters

Most block storage offers ReadWriteOnce only. That single fact explains a large share of Kubernetes storage frustration: a Deployment with two replicas and one ReadWriteOnce claim will schedule one pod and leave the other Pending forever if it lands on a different node, and during a rolling update the new pod waits for the old one to release the volume.

If you need many writers on the same data, you need a filesystem that supports it, not a bigger block device. If you only think you need many writers, check whether the application actually supports concurrent access to its data directory. Most databases do not, and mounting one on a shared filesystem produces corruption rather than scale.

Why StatefulSet Exists

A Deployment treats its pods as interchangeable, which is exactly wrong for storage. Every replica would reference the same claim, and replicas are not supposed to share a disk.

StatefulSet changes three things that matter here. Pods get stable ordinal names rather than random suffixes. Each pod gets its own PVC, created from a volumeClaimTemplate, so replica two keeps its own data. And pod two always reattaches to pod two's volume after a restart or a reschedule.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: pg
spec:
  serviceName: pg
  replicas: 3
  selector:
    matchLabels: { app: pg }
  template:
    metadata:
      labels: { app: pg }
    spec:
      containers:
        - name: pg
          image: postgres:17
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: [ "ReadWriteOnce" ]
        storageClassName: ceph-rbd
        resources:
          requests:
            storage: 50Gi

Two things about that template surprise people. The PVCs it creates are deliberately not deleted when the StatefulSet is deleted, because losing a database because somebody removed a manifest would be indefensible. And volumeClaimTemplates is immutable, so growing the disks of an existing StatefulSet is a resize of each PVC rather than an edit to the template.

Expansion, Reclaim, and Two Settings to Get Right

allowVolumeExpansion: true on the StorageClass. Without it, growing a volume means creating a new one and copying data, under time pressure, on a system that has run out of disk. Set it before you need it; it cannot be applied retroactively to help a claim you are currently trying to expand.

reclaimPolicy, which decides what happens to the PV when its claim is deleted. Delete destroys the underlying storage. Retain keeps it and requires manual cleanup. Use Retain for anything whose loss would be an incident, and accept that you will occasionally have orphaned volumes to tidy.

The other setting worth knowing is volumeBindingMode: WaitForFirstConsumer. With immediate binding, a volume can be created in one availability domain while the pod that needs it gets scheduled somewhere it cannot be attached. Waiting until a pod is scheduled avoids that entirely, and it costs nothing.

A Snapshot Is Not a Backup

The VolumeSnapshot API is genuinely useful and routinely misused as a backup strategy. Two problems. A snapshot usually lives in the same storage system as the volume, so a storage failure takes both. And a snapshot of a running database's disk is a crash-consistent image, which means recovery replays a write-ahead log and may not produce what you expected.

Use snapshots for fast rollback of a deliberate change, such as before a schema migration. Use application-aware dumps written off the cluster for actual recovery. Our guide to incremental versus full backups covers what to run off the cluster, and the point that no backup counts until a restore has been performed.

Picking a Backend

Four patterns cover almost every self-managed cluster.

Local path provisioner. A directory on the node. Fastest possible, zero redundancy, and the data dies with the node. Correct for caches and CI scratch space, wrong for anything you would miss.

Ceph RBD via the CSI driver. Replicated block storage, ReadWriteOnce, survives node loss, and a pod rescheduled to another node reattaches. This is the default answer for databases in a cluster you run yourself.

CephFS or NFS. ReadWriteMany when you genuinely need shared files, such as user uploads served by several web replicas. Slower per operation than block, and worth measuring before committing a latency-sensitive workload to it.

Longhorn or a similar overlay. Replication managed inside Kubernetes, simpler to install than Ceph, and a reasonable middle option on a small cluster with no existing storage layer.

What the Platform Has to Supply

Everything above assumes storage that survives a node failing, because a PV backed by one disk on one machine makes StatefulSets a liability rather than a feature.

MassiveGRID's platform runs Ceph underneath, replicating every block three times across independent NVMe drives, with Proxmox high-availability clustering restarting workloads from a lost node automatically. That combination is what makes ReadWriteOnce claims safe: the volume is not tied to the machine that happened to host the pod. Managed Kubernetes starts at $0.03474 per hour, about $25.37 a month, with cloudlet-based billing at 128 MiB RAM and 400 MHz CPU per unit, and the CSI plumbing is already in place. For the off-cluster copies that snapshots do not replace, backup services use block-level incremental backups with AES-256 encryption at $0.01 per GB.

Storage can be ordered across a partner footprint of more than 700 datacenters in 85 metros, 30 countries and six continents, with auto-provisioning in New York, London, Frankfurt and Singapore.

Further Reading