Kubernetes Persistent Volumes and PVCs Explained
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
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:
- AccessModes — the PVC's requested modes must be a subset of the PV's supported modes
- Storage size — the PV must have at least as much capacity as the PVC requests
- 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
| Mode | Abbreviation | Meaning |
|---|---|---|
ReadWriteOnce | RWO | One node can mount read-write |
ReadOnlyMany | ROX | Many nodes can mount read-only |
ReadWriteMany | RWX | Many nodes can mount read-write |
ReadWriteOncePod | RWOP | One 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.
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 only1# 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: manualkubectl apply -f pv.yaml
kubectl apply -f pvc.yaml
kubectl get pvc my-pvcExpected 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
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-pvckubectl apply -f pod.yaml
kubectl exec my-pod -- cat /data/hello.txt
# helloRestart the pod and check:
kubectl delete pod my-pod
kubectl apply -f pod.yaml
kubectl exec my-pod -- cat /data/hello.txt
# hello ← data survived the restartDynamic 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.
# See available StorageClasses in your cluster
kubectl get storageclassOn EKS you'll see gp2 (the default) and possibly gp3. On GKE, standard and standard-rwo. On AKS, default (Azure Disk) and azurefile.
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: 10Gikubectl apply -f pvc-dynamic.yaml
kubectl get pvc app-dataAfter 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
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: truevolumeBindingMode: 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?
| Policy | Behaviour |
|---|---|
Delete | PV and underlying storage are deleted automatically |
Retain | PV remains, must be manually reclaimed or deleted |
Recycle | Deprecated — 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:
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 volumeThe 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:
kubectl describe pvc my-pvcLook 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:
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: 50GiThis 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.
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 RWODeleting 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.