Kubernetes Persistent Volumes: Complete Guide
Kubernetes Persistent Volumes: Complete Guide
Everything about Kubernetes storage — PVs, PVCs, StorageClasses, and StatefulSets — from basic concepts to production patterns.
Table of Contents
- PV vs PVC vs StorageClass
- Creating a PersistentVolume
- Creating a PersistentVolumeClaim
- Dynamic Provisioning with StorageClass
- Access Modes and Reclaim Policies
- StatefulSets for Stateful Apps
- Common Issues
PV vs PVC vs StorageClass
Three layers of storage abstraction:
| Component | What | Who Creates It |
|---|---|---|
| StorageClass | "Type" of storage (SSD, HDD, Azure Disk) | Admin |
| PersistentVolume (PV) | Actual storage resource | Admin or dynamically |
| PersistentVolumeClaim (PVC) | Pod's request for storage | Developer |
The mental model: StorageClass → PV → PVC → Pod. Like ordering a pizza — the PVC says "I need 10Gi of fast storage" and the StorageClass delivers it.
Creating a PersistentVolume
Static PV (Manual)
apiVersion: v1
kind: PersistentVolume
metadata:
name: data-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce # single node can mount
persistentVolumeReclaimPolicy: Retain # don't delete data
storageClassName: manual
hostPath:
path: /mnt/data # minikube/testing only
hostPath is for testing only — use cloud disks (Azure Disk, AWS EBS, GCE PD) for production.
Cloud-Specific PV (Azure Disk)
apiVersion: v1
kind: PersistentVolume
metadata:
name: azure-disk-pv
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteOnce
azureDisk:
diskName: my-disk
diskURI: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/disks/my-disk
cachingMode: ReadWrite
fsType: ext4
Creating a PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: manual # empty string "" = no dynamic provisioning
Check binding status:
kubectl get pvc
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
# app-data Bound data-pv 10Gi RWO manual
Mount in a Pod
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: nginx
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
volumes:
- name: data
persistentVolumeClaim:
claimName: app-data
Dynamic Provisioning with StorageClass
Modern approach — Kubernetes creates PVs automatically.
Define a StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: disk.csi.azure.com # Azure CSI driver
parameters:
skuName: Premium_LRS # Azure premium SSD
reclaimPolicy: Delete # delete PV when PVC deleted
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
PVC with Dynamic Provisioning
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dynamic-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
storageClassName: fast-ssd # provisions automatically
No PV needed — the provisioner creates it on demand.
Access Modes and Reclaim Policies
Access Modes
| Mode | Meaning | Common Use |
|---|---|---|
ReadWriteOnce (RWO) | One node mounts R/W | Databases, most workloads |
ReadOnlyMany (ROX) | Many nodes read | Static content, configs |
ReadWriteMany (RWX) | Many nodes R/W | Shared file systems (NFS, Azure Files) |
ReadWriteOncePod (RWOP) | One pod R/W | Strict single-pod (K8s 1.27+) |
Reclaim Policies
| Policy | What Happens on PVC Delete |
|---|---|
Retain | PV stays, data preserved |
Delete | PV + underlying storage deleted |
Recycle | Deprecated — use Retain or Delete |
StatefulSets for Stateful Apps
Deployments can't reliably manage state — each pod gets a random name and shared PV. StatefulSets give each pod:
- A stable name (
db-0,db-1,db-2) - Its own PVC via
volumeClaimTemplates - Ordered startup/shutdown
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
storageClassName: fast-ssd
Each replica gets its own data-postgres-0, data-postgres-1, data-postgres-2 PVC.
Common Issues
PVC Stuck in "Pending"
kubectl describe pvc app-data
# Check: no PV matching? StorageClass doesn't exist? Provisioner not running?
Fixes:
- Ensure a PV exists with matching capacity/access mode
- Or ensure
storageClassNamematches a working StorageClass - Check
kubectl get scfor available StorageClasses
"FailedMount" / "FailedAttachVolume"
- Node doesn't have the driver — check CSI driver pods
- Disk already attached to another node (cloud disk single-attach)
- Wait — detach from old node takes a few minutes
Volume Won't Delete
# PV stuck terminating? Check finalizers
kubectl get pv <name> -o yaml | grep finalizers
# Remove the finalizer if you're sure data is safe
kubectl patch pv <name> -p '{"metadata":{"finalizers":null}}'
Running Out of Disk Space
# Check PVC usage
kubectl exec -it <pod> -- df -h /path/to/volume
# Expand if StorageClass supports it
kubectl edit pvc app-data
# change storage: 5Gi → 10Gi (requires allowVolumeExpansion: true)
Quick Reference
# List all PVs and PVCs
kubectl get pv,pvc
# Describe a specific PVC
kubectl describe pvc app-data
# Check which StorageClasses exist
kubectl get sc
# Expand a PVC (if supported)
kubectl patch pvc app-data -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
# See what's mounted inside a pod
kubectl exec -it <pod> -- mount | grep volume
Related Articles
Last Updated: October 2026
Author: CloudOpsGuide Team
Difficulty: Intermediate
Estimated Reading Time: 16 minutes