K8s PV and PVC: Persistent Storage Guide
Create Kubernetes PersistentVolumes and PersistentVolumeClaims. StorageClass, dynamic provisioning, access modes, reclaim policies, and volume expansion.
π‘ Quick Answer: Create a PVC:
kubectl apply -fa PersistentVolumeClaim requesting storage size and access mode. With a StorageClass, volumes are provisioned automatically (dynamic provisioning). Access modes:ReadWriteOnce(single node),ReadOnlyMany(many nodes read),ReadWriteMany(many nodes read/write). Reclaim policies:Delete(default for dynamic) removes data on PVC deletion,Retainkeeps it.
The Problem
Container storage is ephemeral β data is lost when pods restart:
- Database pods lose all data on crash
- Log collectors lose buffered logs
- File uploads disappear on pod reschedule
- No persistent state across deployments
The Solution
Dynamic Provisioning (Recommended)
# 1. StorageClass (usually pre-installed by cloud provider)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: kubernetes.io/aws-ebs # or pd.csi.storage.gke.io, disk.csi.azure.com
parameters:
type: gp3
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
---
# 2. PVC β requests storage
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
---
# 3. Pod using PVC
apiVersion: v1
kind: Pod
metadata:
name: postgres
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-dataStatic Provisioning
# Admin creates PV manually
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
server: nfs.example.com
path: /exports/data
---
# PVC binds to matching PV
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: shared-data
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi
storageClassName: "" # Empty = no dynamic provisioningAccess Modes
| Mode | Abbreviation | Description |
|---|---|---|
| ReadWriteOnce | RWO | Single node read/write |
| ReadOnlyMany | ROX | Multiple nodes read-only |
| ReadWriteMany | RWX | Multiple nodes read/write |
| ReadWriteOncePod | RWOP | Single pod (K8s 1.27+) |
# Check access modes supported by StorageClass
kubectl get storageclass
kubectl describe storageclass fast-ssdVolume Expansion
# Expand PVC (StorageClass must have allowVolumeExpansion: true)
kubectl patch pvc postgres-data -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'
# Check status
kubectl get pvc postgres-data
# NAME STATUS VOLUME CAPACITY ACCESS MODES
# postgres-data Bound pv-xxx 100Gi RWO
# Some CSI drivers require pod restart for filesystem expansion
kubectl delete pod postgresStatefulSet with VolumeClaimTemplates
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
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates: # Auto-creates PVC per replica
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 50Gi
# Creates: data-postgres-0, data-postgres-1, data-postgres-2Common Issues
PVC stuck in Pending
No matching PV or StorageClass not configured. Check: kubectl describe pvc <name> for events. See troubleshooting-pending-pvc.
Data lost after pod restart
Volume not mounted or using emptyDir instead of PVC. Verify: kubectl describe pod <name> | grep -A5 Volumes.
βvolume is already exclusively attachedβ
RWO volume canβt attach to multiple nodes. Pod must schedule on same node, or use RWX access mode.
Best Practices
- Always use dynamic provisioning β let StorageClass handle PV creation
- Set
WaitForFirstConsumerβ binds volume to podβs node (topology-aware) - Use
Retainfor databases β donβt auto-delete production data - StatefulSet + volumeClaimTemplates β one PVC per replica automatically
- Monitor PVC usage with
kubelet_volume_stats_used_bytesPrometheus metric
Key Takeaways
- PVCs request storage; PVs provide it; StorageClass automates provisioning
- Dynamic provisioning with StorageClass is the standard approach
- RWO for databases, RWX for shared filesystems, RWOP for strict single-pod
- Volume expansion supported with
allowVolumeExpansion: truein StorageClass - StatefulSet
volumeClaimTemplatescreate per-replica persistent storage

Recommended
Kubernetes Recipes β The Complete Book100+ production-ready patterns with detailed explanations, best practices, and copy-paste YAML. Everything in one place.
Get the Book βLearn by Doing
CopyPasteLearn β Hands-on Cloud & DevOps CoursesMaster Kubernetes, Ansible, Terraform, and MLOps with interactive, copy-paste-run lessons. Start free.
Browse Courses βπ Deepen Your Skills β Hands-on Courses
Courses by CopyPasteLearn.com β Learn IT by Doing
