Kubernetes

Kubernetes Persistent Volumes and PVCs Explained

Beginner30 min to complete10 min read

Pods are ephemeral — their local storage disappears when they restart. Persistent Volumes decouple storage from pod lifecycle. This tutorial covers PV, PVC, and StorageClass from scratch, with real examples and the common mistakes that cause PVCs to stay stuck in Pending.

Before you begin

  • kubectl configured against a running cluster
  • Basic familiarity with Pods and Deployments
Kubernetes
Storage
PersistentVolume
PVC
StatefulSets
DevOps

Pods are ephemeral. When a pod restarts — due to a crash, a node eviction, or a rolling update — its local filesystem is wiped. Any data written to the container's writable layer is gone.

Kubernetes solves this with three resources that work together:

  • PersistentVolume (PV) — a piece of storage provisioned in the cluster (manually by an admin, or automatically by a StorageClass)
  • PersistentVolumeClaim (PVC) — a request for storage made by a pod, specifying size and access mode
  • StorageClass — defines how storage is provisioned dynamically (the provisioner, the disk type, reclaim policy)

How Binding Works

A PVC doesn't point directly at a PV. Kubernetes binds them by matching:

  1. AccessModes — the PVC's requested modes must be a subset of the PV's supported modes
  2. Storage size — the PV must have at least as much capacity as the PVC requests
  3. StorageClassName — must match exactly (or both must be empty for static binding)

Once a PV is bound to a PVC, that PVC is exclusively bound — no other PVC can use it until it's released.

AccessModes Reference

ModeAbbreviationMeaning
ReadWriteOnceRWOOne node can mount read-write
ReadOnlyManyROXMany nodes can mount read-only
ReadWriteManyRWXMany nodes can mount read-write
ReadWriteOncePodRWOPOne pod can mount read-write (K8s 1.22+)

Most cloud block storage (EBS, GCE PD, Azure Disk) only supports ReadWriteOnce. For ReadWriteMany, you need NFS, CephFS, or a managed file storage service (EFS, Azure Files, Filestore).

Static Provisioning

In static provisioning, an administrator creates PVs manually and developers claim them via PVCs.

yaml
1# pv.yaml — administrator creates this
2apiVersion: v1
3kind: PersistentVolume
4metadata:
5  name: my-pv
6spec:
7  capacity:
8    storage: 5Gi
9  accessModes:
10    - ReadWriteOnce
11  persistentVolumeReclaimPolicy: Retain
12  storageClassName: manual
13  hostPath:
14    path: /data/my-pv   # local path on the node — for testing only
yaml
1# pvc.yaml — developer creates this
2apiVersion: v1
3kind: PersistentVolumeClaim
4metadata:
5  name: my-pvc
6  namespace: default
7spec:
8  accessModes:
9    - ReadWriteOnce
10  resources:
11    requests:
12      storage: 2Gi
13  storageClassName: manual
bash
kubectl apply -f pv.yaml
kubectl apply -f pvc.yaml
kubectl get pvc my-pvc

Expected output:

NAME     STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
my-pvc   Bound    my-pv    5Gi        RWO            manual         5s

STATUS: Bound means Kubernetes matched the PVC to the PV. The PV has 5Gi, the PVC requested 2Gi — that's fine. A PV can satisfy a smaller request; a PV cannot satisfy a larger one.

Using a PVC in a Pod

yaml
1apiVersion: v1
2kind: Pod
3metadata:
4  name: my-pod
5spec:
6  containers:
7    - name: app
8      image: busybox
9      command: ["sh", "-c", "echo 'hello' > /data/hello.txt && sleep 3600"]
10      volumeMounts:
11        - mountPath: /data
12          name: my-storage
13  volumes:
14    - name: my-storage
15      persistentVolumeClaim:
16        claimName: my-pvc
bash
kubectl apply -f pod.yaml
kubectl exec my-pod -- cat /data/hello.txt
# hello

Restart the pod and check:

bash
kubectl delete pod my-pod
kubectl apply -f pod.yaml
kubectl exec my-pod -- cat /data/hello.txt
# hello    ← data survived the restart

Dynamic Provisioning with StorageClass

Static provisioning doesn't scale — admins can't pre-create a PV for every service. StorageClasses allow Kubernetes to provision storage on demand.

bash
# See available StorageClasses in your cluster
kubectl get storageclass

On EKS you'll see gp2 (the default) and possibly gp3. On GKE, standard and standard-rwo. On AKS, default (Azure Disk) and azurefile.

yaml
1# pvc-dynamic.yaml — no storageClassName means use the default
2apiVersion: v1
3kind: PersistentVolumeClaim
4metadata:
5  name: app-data
6  namespace: default
7spec:
8  accessModes:
9    - ReadWriteOnce
10  resources:
11    requests:
12      storage: 10Gi
bash
kubectl apply -f pvc-dynamic.yaml
kubectl get pvc app-data

After a few seconds:

NAME       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
app-data   Bound    pvc-a1b2c3d4-...                          10Gi       RWO            gp2            8s

Kubernetes called the gp2 provisioner (the EBS CSI driver), provisioned a 10Gi EBS volume, created a PV for it, and bound it — all automatically.

Defining a Custom StorageClass

yaml
1apiVersion: storage.k8s.io/v1
2kind: StorageClass
3metadata:
4  name: fast-ssd
5provisioner: ebs.csi.aws.com
6parameters:
7  type: gp3
8  iops: "6000"
9  throughput: "250"
10reclaimPolicy: Delete
11volumeBindingMode: WaitForFirstConsumer
12allowVolumeExpansion: true

volumeBindingMode: WaitForFirstConsumer is important for EBS — it delays PV creation until a pod using the PVC is scheduled, so the EBS volume is created in the same availability zone as the node.

allowVolumeExpansion: true lets you resize PVCs later without deleting data.

Reclaim Policies

What happens to the PV when a PVC is deleted?

PolicyBehaviour
DeletePV and underlying storage are deleted automatically
RetainPV remains, must be manually reclaimed or deleted
RecycleDeprecated — do not use

For production data, use Retain until you've confirmed the data is no longer needed. For ephemeral scratch space, Delete is fine.

Expanding a PVC

If the StorageClass has allowVolumeExpansion: true, you can resize an existing PVC:

bash
kubectl patch pvc app-data -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
kubectl get pvc app-data
# CAPACITY shows 20Gi after the CSI driver resizes the volume

The pod doesn't need to restart for online resize (supported by gp3 and most modern CSI drivers).

Debugging: PVC Stuck in Pending

The most common reason a PVC stays in Pending:

bash
kubectl describe pvc my-pvc

Look at the Events section. Common causes:

No matching PV (static provisioning):

Events: Warning  FailedBinding  no persistent volumes available for this claim

→ Check that the PV's storageClassName, accessModes, and capacity match the PVC.

StorageClass provisioner unavailable (dynamic provisioning):

Events: Warning  ProvisioningFailed  failed to provision volume: ...

→ Check that the CSI driver is installed and the IAM role (for EBS CSI on EKS) has permissions.

WaitForFirstConsumer waiting for pod:

Events: Normal   WaitForFirstConsumer  waiting for first consumer to be created before binding

→ This is not an error. The PVC will bind once a pod that uses it is scheduled.

Using PVCs with StatefulSets

StatefulSets have a volumeClaimTemplates field that creates a dedicated PVC per pod replica automatically:

yaml
1apiVersion: apps/v1
2kind: StatefulSet
3metadata:
4  name: postgres
5spec:
6  serviceName: postgres
7  replicas: 3
8  selector:
9    matchLabels:
10      app: postgres
11  template:
12    metadata:
13      labels:
14        app: postgres
15    spec:
16      containers:
17        - name: postgres
18          image: postgres:16
19          env:
20            - name: PGDATA
21              value: /var/lib/postgresql/data/pgdata
22          volumeMounts:
23            - name: data
24              mountPath: /var/lib/postgresql/data
25  volumeClaimTemplates:
26    - metadata:
27        name: data
28      spec:
29        accessModes: ["ReadWriteOnce"]
30        storageClassName: fast-ssd
31        resources:
32          requests:
33            storage: 50Gi

This creates three PVCs: data-postgres-0, data-postgres-1, data-postgres-2. Each pod mounts its own dedicated volume — they don't share storage. When you scale down and scale back up, the same PVC is reattached to the same pod identity.

bash
kubectl get pvc -l app=postgres
# NAME             STATUS   VOLUME        CAPACITY   ACCESS MODES
# data-postgres-0  Bound    pvc-...       50Gi       RWO
# data-postgres-1  Bound    pvc-...       50Gi       RWO
# data-postgres-2  Bound    pvc-...       50Gi       RWO

Deleting the StatefulSet does not delete these PVCs. You must delete them manually, which is intentional protection against accidental data loss.

We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.

Struggling with this in production?

We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.

Go deeper

Kubernetes StatefulSets and Persistent Storage: Patterns for Stateful WorkloadsRunning stateful workloads in Kubernetes — databases, message queues, caches — requires stable network identity, ordered deployment, and persistent volumes that survive pod restarts. StatefulSets provide the first two; StorageClasses and PersistentVolumeClaims handle the third. Together they make Kubernetes a viable home for workloads that traditionally required VMs.
13 min
Kubernetes Storage: EBS and EFS CSI Drivers on EKSThe EBS CSI driver provisions gp3 volumes for StatefulSets and ReadWriteOnce workloads. The EFS CSI driver provides ReadWriteMany persistent storage for workloads that need shared access across pods and nodes. On EKS, both run as managed add-ons with IRSA for IAM access. This covers the full setup: StorageClass configuration, PVC provisioning, EFS access points for multi-tenant isolation, and the operational patterns for resizing, backups, and cross-AZ scheduling.
12 min
Kubernetes StatefulSets: Running Stateful Workloads in ProductionStatefulSets give pods stable network identities (pod-0, pod-1) and stable storage (PersistentVolumeClaims that survive pod rescheduling). This makes them the right primitive for databases, Kafka brokers, ZooKeeper ensembles, and any workload where identity and data persistence matter. This covers StatefulSet mechanics, volumeClaimTemplates for per-pod storage, headless Services for DNS-based peer discovery, ordered vs parallel pod management, PodDisruptionBudgets for controlled maintenance, and the operational patterns for running PostgreSQL and Kafka in Kubernetes.
14 min