CloudOpsGuide
kubernetes

Kubernetes Persistent Volumes: Complete Guide

Intermediate
16 minutes
October 2026
CloudOpsGuide Team

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

Three layers of storage abstraction:

ComponentWhatWho Creates It
StorageClass"Type" of storage (SSD, HDD, Azure Disk)Admin
PersistentVolume (PV)Actual storage resourceAdmin or dynamically
PersistentVolumeClaim (PVC)Pod's request for storageDeveloper

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

ModeMeaningCommon Use
ReadWriteOnce (RWO)One node mounts R/WDatabases, most workloads
ReadOnlyMany (ROX)Many nodes readStatic content, configs
ReadWriteMany (RWX)Many nodes R/WShared file systems (NFS, Azure Files)
ReadWriteOncePod (RWOP)One pod R/WStrict single-pod (K8s 1.27+)

Reclaim Policies

PolicyWhat Happens on PVC Delete
RetainPV stays, data preserved
DeletePV + underlying storage deleted
RecycleDeprecated — 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 storageClassName matches a working StorageClass
  • Check kubectl get sc for 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