Kubernetes

Kubernetes Persistent Volumes and PVCs Explained

Beginner30 min to complete10 min readMay 29, 2026Updated August 19, 2026

Quick answer

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.

beginner · 30 min

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.

Frequently Asked Questions

When would I use static provisioning?

When the storage already exists and you are attaching it — an NFS export, a pre-created cloud disk with data on it, or hardware with specific characteristics. Dynamic provisioning is the default for new volumes; static is for adopting storage that has a life outside the cluster.

Why is my PVC Pending with no events?

With volume binding mode set to wait for the first consumer, a Pending PVC is expected until a pod is scheduled — the provisioner delays creation so the volume lands in the right zone. If no pod is waiting on it, nothing will happen. That mode prevents a volume being created where nothing can use it.

What does the Retain reclaim policy leave behind?

The PersistentVolume and the underlying storage, in a Released state that cannot be bound by a new claim until you clear the old claim reference. That manual step is deliberate — it stops a new PVC accidentally binding to a volume holding someone else's data.

Can several pods share one volume?

Only if the access mode and the storage support it. ReadWriteOnce permits one node at a time, so pods on the same node can share it and pods elsewhere cannot. ReadWriteMany allows multiple nodes but most block storage does not implement it — that generally means a file storage backend.

Official References

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.