Home DevOps Kubernetes Persistent Volumes and Storage Classes: Stop Losing State When Pods Die
Intermediate 4 min · July 18, 2026

Kubernetes Persistent Volumes and Storage Classes: Stop Losing State When Pods Die

Kubernetes persistent volumes and storage classes explained with production patterns.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Production
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 30 min
  • Basic Kubernetes pod and deployment syntax
  • Understanding of YAML manifests
  • Familiarity with kubectl commands
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Use a StorageClass to dynamically provision PersistentVolumes when a PVC is created. Set reclaimPolicy to Retain to prevent data loss. Always test with a dummy workload before production.

✦ Definition~90s read
What is Kubernetes Persistent Volumes and Storage Classes?

A PersistentVolume (PV) is a cluster-wide storage resource provisioned by an admin or dynamically via a StorageClass. A PersistentVolumeClaim (PVC) is a request for storage by a user. StorageClasses define the type of storage (e.g., SSD, network) and provisioning behavior (static vs dynamic).

Think of a PersistentVolume as a reserved parking spot in a giant lot.
Plain-English First

Think of a PersistentVolume as a reserved parking spot in a giant lot. A PersistentVolumeClaim is your parking permit—it says 'I need a spot that fits my car.' The StorageClass is the lot manager who decides whether to assign an existing spot (static) or build a new one (dynamic) when you show up. If your car (pod) gets towed, the spot stays reserved until you clean it up.

Every Kubernetes newbie learns the hard way: pods are ephemeral, but your database isn't. I've seen a startup lose 3 days of user data because they assumed a StatefulSet's PVC would survive a node failure. It didn't. PersistentVolumes and StorageClasses are how you tell Kubernetes 'this data matters.' Without them, you're gambling with state.

The problem is simple: containers crash, nodes fail, clusters scale down. Without persistent storage, your Postgres data vanishes when the pod restarts. The hack people used was hostPath—binding to a directory on the node. That works until your pod reschedules to a different node. Then you're staring at an empty directory and a corrupted database.

By the end of this, you'll be able to configure dynamic provisioning with a StorageClass, set reclaim policies that prevent accidental data deletion, and debug the three most common PV failures I've seen in production. You'll also know when to skip PVs entirely and use a managed database instead.

Why Static Provisioning Will Burn You

Before StorageClasses, you had to create PVs manually. Every developer who needed storage had to pester the ops team: 'Hey, can you provision a 50GB EBS volume and create a PV for it?' That's slow, error-prone, and doesn't scale. Worse, when the PV was full, you couldn't resize it without downtime. I've seen a team delete a production PV by accident because they forgot to set persistentVolumeReclaimPolicy: Retain. The PVC was deleted, and boom—the PV was recycled. Data gone.

Dynamic provisioning via StorageClass automates this. You define a StorageClass that points to a provisioner (e.g., kubernetes.io/aws-ebs), and when a PVC is created, Kubernetes calls the cloud provider's API to create the volume and bind it automatically. No manual steps. No ops bottleneck.

But here's the catch: not all StorageClasses are created equal. Some provisioners (like kubernetes.io/host-path or local) don't support dynamic provisioning at all. Always check the provisioner's documentation. For production, use cloud-native provisioners (EBS, GCE PD, Azure Disk) or a CSI driver like Rook/Ceph.

dynamic-provisioning.yamlDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// io.thecodeforge — DevOps tutorial

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp3
  fsType: ext4
  encrypted: "true"
reclaimPolicy: Retain  # Don't delete volume when PVC is deleted
volumeBindingMode: WaitForFirstConsumer  # Delay provisioning until pod scheduled
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-data
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 50Gi
  storageClassName: fast-ssd
Output
StorageClass 'fast-ssd' created. PVC 'mysql-data' will be bound once a pod uses it.
⚠ Production Trap: ReclaimPolicy Delete
If you set reclaimPolicy: Delete (the default), deleting the PVC will delete the underlying cloud volume. I've seen a team lose a 500GB Postgres volume this way. Always set Retain for production databases, and manually clean up volumes after verifying backups.

Access Modes: ReadWriteOnce vs ReadOnlyMany vs ReadWriteMany

Access modes define how many pods can mount the volume and with what permissions. ReadWriteOnce (RWO) is the most common—one pod can read and write. ReadOnlyMany (ROX) allows many pods to read simultaneously. ReadWriteMany (RWX) allows many pods to read and write—but this is the trickiest.

RWX requires a filesystem that supports concurrent writes, like NFS or GlusterFS. Cloud block storage (EBS, GCE PD) only supports RWO. I've seen teams try to use an EBS volume with RWX and wonder why their pods crash with mount.nfs: access denied by server while mounting. The fix is to use a CSI driver that supports RWX, like Amazon EFS or Azure Files.

For stateful workloads like databases, use RWO. For shared configuration or logs, use ROX or RWX with a proper distributed filesystem. Never assume RWX works—test it first.

access-modes-pvc.yamlDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
// io.thecodeforge — DevOps tutorial

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shared-logs
spec:
  accessModes:
    - ReadWriteMany  # Requires a volume that supports RWX (e.g., NFS, EFS)
  resources:
    requests:
      storage: 10Gi
  storageClassName: efs-sc
Output
PVC 'shared-logs' created. Pods can mount it concurrently only if the underlying volume supports RWX.
🔥Senior Shortcut: Check Volume Capabilities
Run kubectl get storageclass <name> -o yaml and look for allowVolumeExpansion: true and mountOptions. For RWX, ensure the provisioner supports it. For EBS, it doesn't—use EFS or a CSI driver.

Reclaim Policies: Retain, Delete, or Recycle

When a PVC is deleted, what happens to the PV? That's the reclaim policy. Delete removes the PV and the underlying storage asset. Retain keeps the PV and volume—you must manually delete them. Recycle (deprecated) scrubs the volume and makes it available again.

In production, always use Retain for any volume with data you care about. I've seen a junior engineer delete a PVC thinking it would just unbind the PV—but the policy was Delete, and the entire EBS volume was gone. No recovery. The fix is to set persistentVolumeReclaimPolicy: Retain on the StorageClass or on individual PVs.

For ephemeral storage (e.g., build artifacts), Delete is fine. But for databases, queues, or any stateful service, Retain is non-negotiable. And always have a backup strategy—Retain doesn't protect against accidental deletion of the PV itself.

pv-with-retain.yamlDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — DevOps tutorial

apiVersion: v1
kind: PersistentVolume
metadata:
  name: manual-pv
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain  # Volume survives PVC deletion
  awsElasticBlockStore:
    volumeID: vol-0a1b2c3d4e5f67890
    fsType: ext4
Output
PV 'manual-pv' created with Retain policy. Deleting the PVC will leave the PV and volume intact.
⚠ Never Do This: Rely on Recycle
Recycle policy is deprecated in Kubernetes 1.20+. It used to run a pod that scrubbed the volume, but it was unreliable and insecure. Use Retain and manually clean up, or use a StorageClass with Delete for disposable data.

Volume Binding Modes: Immediate vs WaitForFirstConsumer

Volume binding mode controls when the PV is provisioned and bound to the PVC. Immediate creates the volume as soon as the PVC is created. WaitForFirstConsumer delays provisioning until a pod using the PVC is scheduled.

Why use WaitForFirstConsumer? In multi-zone clusters, Immediate might provision a volume in zone A, but the pod gets scheduled in zone B. Then the pod can't mount the volume because it's in a different availability zone. I've seen this bring down a multi-region deployment—pods were stuck in ContainerCreating with volume zone mismatch errors.

WaitForFirstConsumer ensures the volume is provisioned in the same zone as the pod. This is critical for cloud providers where volumes are zonal (EBS, GCE PD). For regional volumes (like Azure Disk), Immediate is fine. Always set volumeBindingMode: WaitForFirstConsumer for zonal storage.

storageclass-wffc.yamlDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
// io.thecodeforge — DevOps tutorial

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: zonal-ssd
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-ssd
  zones: us-central1-a,us-central1-b
volumeBindingMode: WaitForFirstConsumer  # Provision in same zone as pod
reclaimPolicy: Retain
Output
StorageClass 'zonal-ssd' created. PVCs using this class will only bind when a pod is scheduled, ensuring zone affinity.
💡Interview Gold: Zone Affinity
If you're asked 'How do you ensure a pod can mount its volume in a multi-zone cluster?' the answer is WaitForFirstConsumer binding mode. Without it, you risk cross-zone mounting failures.

Expanding Volumes Without Downtime

You deployed a 50GB Postgres instance, and now it's at 48GB. You need more space. With static provisioning, you'd have to create a new PV, migrate data, and update the PVC—downtime guaranteed. With dynamic provisioning and allowVolumeExpansion: true, you can edit the PVC's storage request and Kubernetes will expand the volume in place.

But there are caveats. Not all volume types support expansion. EBS gp2/gp3 does, but magnetic (standard) doesn't. Also, the filesystem must support resizing—ext4 and xfs do, but not all CSI drivers handle it automatically. I've seen a team expand a PVC, but the pod still saw the old size because the filesystem wasn't resized. The fix was to run resize2fs inside the pod (or use a CSI driver that does it automatically).

To enable expansion, set allowVolumeExpansion: true in the StorageClass. Then edit the PVC: kubectl edit pvc <name> and change spec.resources.requests.storage. The volume will expand, but the pod may need a restart to see the new size.

expand-pvc.shDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — DevOps tutorial

# Step 1: Enable expansion in StorageClass
kubectl patch storageclass standard -p '{"allowVolumeExpansion": true}'

# Step 2: Increase PVC size
kubectl patch pvc mysql-data -p '{"spec": {"resources": {"requests": {"storage": "100Gi"}}}}'

# Step 3: Verify expansion
kubectl get pvc mysql-data -o jsonpath='{.status.capacity.storage}'
# Output: 100Gi

# Step 4: If filesystem not resized, exec into pod and run:
kubectl exec mysql-0 -- resize2fs /var/lib/mysql
Output
PVC expanded to 100Gi. Filesystem resized automatically if the CSI driver supports it.
⚠ Production Trap: Expansion Without Filesystem Resize
After expanding the PVC, always check the pod's mount: df -h. If the old size shows, the filesystem wasn't resized. For ext4, run resize2fs inside the pod. For xfs, use xfs_growfs. Some CSI drivers (like EBS) do this automatically, but don't assume.

When Not to Use Persistent Volumes

PVs are great for stateful workloads, but they're not the answer to every storage problem. If you're running a stateless application, don't use PVs—use emptyDir or ConfigMap. If you need a database, consider a managed service (RDS, Cloud SQL) instead of running it yourself on PVs. Managed databases handle backups, replication, and failover—you don't have to.

I've seen teams run MySQL on PVs with a single replica, no backup strategy, and wonder why they lost data when the node died. For production databases, use a managed service or at least a StatefulSet with regular backups. PVs are infrastructure, not a backup solution.

Also, avoid PVs for temporary or cache data. Use emptyDir with a memory-backed medium (if your node has enough RAM) or a sidecar with a caching layer like Redis. PVs add latency and complexity—only use them when data must survive pod restarts.

emptydir-example.yamlDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge — DevOps tutorial

apiVersion: v1
kind: Pod
metadata:
  name: cache-pod
spec:
  containers:
  - name: app
    image: myapp:latest
    volumeMounts:
    - mountPath: /cache
      name: temp-storage
  volumes:
  - name: temp-storage
    emptyDir:
      medium: Memory  # Use RAM for speed
      sizeLimit: 1Gi
Output
Pod uses a memory-backed emptyDir for temporary cache. Data is lost when pod terminates—intentional.
💡Senior Shortcut: Use emptyDir for Logs
If your app writes logs that don't need to survive crashes, use emptyDir. For logs that must persist, use a sidecar that ships them to a central system (e.g., Fluentd to S3). Don't use PVs for logs—they'll fill up and cause pod evictions.

Debugging PV Failures: The Three Most Common Issues

  1. PVC stuck in Pending: Run kubectl describe pvc <name>. Look for events like no persistent volumes available for this claim and no storage class is set. Fix: add storageClassName to PVC or create a default StorageClass.
  2. Pod stuck in ContainerCreating: Run kubectl describe pod <name>. Look for volume node affinity conflict or multi-attach error. Fix: ensure volume is in the same zone as the pod (use WaitForFirstConsumer), or delete the pod that's holding the volume (if multi-attach not supported).
  3. Volume mount fails with permission denied: Check the filesystem permissions. Many PVs (like NFS) mount as root. If your pod runs as non-root, you need to set fsGroup in the pod's security context. I've seen this with Jenkins agents—they couldn't write to the workspace because the PV was owned by root.
debug-pvc.shDEVOPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — DevOps tutorial

# Check PVC status and events
kubectl describe pvc mysql-data

# Check pod events for volume issues
kubectl describe pod mysql-0

# Check if StorageClass exists
kubectl get storageclass

# Check PV details
kubectl get pv
kubectl describe pv <pv-name>

# For permission issues, add fsGroup to pod spec:
kubectl patch deployment mysql -p '{"spec":{"template":{"spec":{"securityContext":{"fsGroup":1000}}}}}'
Output
Debug commands output PVC and pod events, revealing the root cause.
🔥The Classic Bug: fsGroup Not Set
If your pod runs as non-root and can't write to the PV, you forgot securityContext.fsGroup. This is a common issue with NFS and EFS volumes. Set it to the group ID that owns the mount directory.
● Production incidentPOST-MORTEMseverity: high

The 4GB Container That Kept Dying

Symptom
A Redis pod kept crashing with OOMKill every 6 hours. The team restarted it, and it worked for another 6 hours.
Assumption
Memory leak in Redis. They increased memory limits to 8GB.
Root cause
Redis was configured to persist to disk every 5 minutes (save 300 1). The pod was using a hostPath volume on a node with only 4GB free. When Redis triggered a BGSAVE, it forked and doubled memory usage, hitting the node's disk limit and causing the OOM killer to kill the pod.
Fix
Switched to a PersistentVolume backed by a network filesystem (NFS) with 100GB. Changed Redis config to save 900 1 to reduce fork frequency. Set resources.requests.memory: 2Gi and limits.memory: 4Gi to prevent fork from exceeding node memory.
Key lesson
  • hostPath is a trap for stateful workloads.
  • Always use network-backed PVs for databases, and always account for fork overhead in memory limits.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
PVC stuck in Pending with event: no persistent volumes available for this claim and no storage class is set
Fix
1. Run kubectl get storageclass to list available StorageClasses. 2. If none exist, create one: kubectl apply -f storageclass.yaml. 3. If one exists, add storageClassName: <name> to the PVC spec. 4. If you want a default, annotate a StorageClass: kubectl annotate storageclass <name> storageclass.kubernetes.io/is-default-class=true.
Symptom · 02
Pod stuck in ContainerCreating with event: volume node affinity conflict
Fix
1. Check the PV's node affinity: kubectl get pv <pv-name> -o yaml | grep nodeAffinity. 2. Check the pod's node selector or affinity. 3. If the volume is in a different zone, delete the PVC and recreate it with a StorageClass that has volumeBindingMode: WaitForFirstConsumer. 4. Or delete the PV and recreate it in the correct zone.
Symptom · 03
Pod fails to mount volume with permission denied
Fix
1. Check the pod's security context: kubectl get pod <pod-name> -o yaml | grep -A5 securityContext. 2. If fsGroup is missing, add it: kubectl patch deployment <deploy> -p '{"spec":{"template":{"spec":{"securityContext":{"fsGroup":1000}}}}}'. 3. If the volume is NFS, ensure the export permissions allow the pod's UID/GID.
★ Kubernetes Persistent Volumes and Storage Classes Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
PVC stuck in Pending with `no persistent volumes available`
Immediate action
Check if a StorageClass exists and is default
Commands
kubectl get storageclass
kubectl describe pvc <name>
Fix now
kubectl patch storageclass <name> -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
Pod stuck in ContainerCreating with `volume node affinity conflict`+
Immediate action
Check PV zone and pod node
Commands
kubectl get pv -o wide | grep <pvc-name>
kubectl get pod <pod-name> -o wide
Fix now
Delete PVC and recreate with StorageClass that has volumeBindingMode: WaitForFirstConsumer
Pod can't write to mounted volume (permission denied)+
Immediate action
Check pod securityContext
Commands
kubectl get pod <pod-name> -o yaml | grep -A10 securityContext
kubectl exec <pod-name> -- ls -la /mount-path
Fix now
kubectl patch deployment <deploy> -p '{"spec":{"template":{"spec":{"securityContext":{"fsGroup":1000}}}}}'
Volume expansion not reflected in pod+
Immediate action
Check filesystem size inside pod
Commands
kubectl exec <pod-name> -- df -h /mount-path
kubectl get pvc <name> -o jsonpath='{.status.capacity.storage}'
Fix now
kubectl exec <pod-name> -- resize2fs /mount-path (for ext4) or xfs_growfs /mount-path (for xfs)
FeatureStatic ProvisioningDynamic Provisioning (StorageClass)
Setup effortManual: create PV, then PVCAutomatic: just create PVC
ScalabilityLow: ops bottleneckHigh: on-demand
Volume expansionNot supported (must migrate)Supported with allowVolumeExpansion
Reclaim flexibilityPer-PV policyPer-StorageClass policy
Zone affinityManual zone selectionAutomatic with WaitForFirstConsumer
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
dynamic-provisioning.yamlapiVersion: storage.k8s.io/v1Why Static Provisioning Will Burn You
access-modes-pvc.yamlapiVersion: v1Access Modes
pv-with-retain.yamlapiVersion: v1Reclaim Policies
storageclass-wffc.yamlapiVersion: storage.k8s.io/v1Volume Binding Modes
expand-pvc.shkubectl patch storageclass standard -p '{"allowVolumeExpansion": true}'Expanding Volumes Without Downtime
emptydir-example.yamlapiVersion: v1When Not to Use Persistent Volumes
debug-pvc.shkubectl describe pvc mysql-dataDebugging PV Failures

Key takeaways

1
Always set reclaimPolicy
Retain on StorageClasses for production data—one accidental PVC deletion can wipe your database.
2
Use volumeBindingMode
WaitForFirstConsumer for zonal cloud volumes to avoid cross-zone mounting failures.
3
Dynamic provisioning via StorageClass eliminates the ops bottleneck of manual PV creation—use it unless you have a specific reason not to.
4
PVs are not backups. Even with Retain, you need a separate backup strategy (e.g., Velero, cloud snapshots).
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does Kubernetes handle volume provisioning when a PVC uses a Storage...
Q02SENIOR
When would you choose a StatefulSet with a VolumeClaimTemplate over a De...
Q03SENIOR
What happens when you delete a PVC that is bound to a PV with reclaimPol...
Q04JUNIOR
What is the difference between a PersistentVolume and a hostPath volume?
Q05SENIOR
You have a PVC that is stuck in Pending with the event 'waiting for firs...
Q06SENIOR
How would you design a storage solution for a multi-tenant SaaS applicat...
Q01 of 06SENIOR

How does Kubernetes handle volume provisioning when a PVC uses a StorageClass with volumeBindingMode: WaitForFirstConsumer and the pod is scheduled on a node in a different zone than the volume?

ANSWER
WaitForFirstConsumer delays provisioning until the pod is scheduled. The scheduler then picks a node, and the volume is provisioned in the same zone as that node. If the pod is rescheduled to a different zone, the original volume is deleted and a new one is created in the new zone. This ensures zone affinity but can cause extra API calls.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between a PersistentVolume and a PersistentVolumeClaim?
02
What's the difference between static and dynamic provisioning?
03
How do I resize a PersistentVolumeClaim?
04
What happens to data when a pod using a PersistentVolumeClaim is deleted?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
🔥

That's Kubernetes. Mark it forged?

4 min read · try the examples if you haven't

Previous
Kubernetes Namespaces and ResourceQuotas
33 / 43 · Kubernetes
Next
Kubernetes Persistent Volume Claims: Dynamic Provisioning