Kubernetes Persistent Volumes, PVCs and dynamic provisioning explained
Learn how Kubernetes PersistentVolumes and PVCs work. Covers concepts, static provisioning with a working example, StorageClasses, and dynamic provisioning with local-path-provisioner.
5 lessons · 16 min · Intermediate
Written by
Marketing Team at Civo
Written by
Marketing Team at Civo
Pods are ephemeral. When a pod is deleted and recreated, any data written to its local filesystem is gone. PersistentVolumes solve this by providing storage that exists independently of the pod lifecycle. A pod can be deleted, rescheduled to a different node, and reconnected to the same storage when it comes back up.
Core concepts
PersistentVolume (PV)
A PersistentVolume is a storage resource provisioned in the cluster. It is an abstraction of the underlying storage backend, whether that is a local disk, an NFS share, a cloud block device, or any other storage system. PVs are cluster-scoped objects, not tied to any namespace.
PersistentVolumeClaim (PVC)
A PersistentVolumeClaim is a request for storage made by a user or workload. The user specifies how much storage they need, which access mode they require, and optionally which StorageClass to use. Kubernetes matches the PVC to an available PV that satisfies those requirements and binds them together.
The binding lifecycle
The control plane continuously watches for unbound PVCs and tries to find a matching PV. Once a match is found, the PVC moves to Bound status and the PV is reserved exclusively for that PVC. The PVC and PV have their own lifecycle separate from any pod. Deleting a pod does not delete the PVC or PV.
Access modes
Not all storage backends support all access modes. Check your storage provider's documentation.
Reclaim policies
The Recycle policy is deprecated and should not be used.
Static vs dynamic provisioning
Static provisioning: an admin creates PVs manually before any PVC exists. The PVC binds to a matching manually created PV.
Dynamic provisioning: a StorageClass with a provisioner automatically creates a PV whenever a PVC requests one. The developer only specifies what they need. The cluster handles the rest.
Static provisioning: working example
Note: this example uses hostPath as the underlying storage for simplicity. hostPath should not be used in production. See the hostPath volumes lesson for security context.
Create the PersistentVolume
Before running this on a node, create the directory and an index.html file:
mkdir -p /mnt/dataecho "hello" > /mnt/data/index.html
apiVersion: v1kind: PersistentVolumemetadata:name: demo-pvspec:storageClassName: manualcapacity:storage: 3GiaccessModes:- ReadWriteOncehostPath:path: /mnt/datapersistentVolumeReclaimPolicy: Retain
kubectl create -f pv.yaml
Create the PersistentVolumeClaim
apiVersion: v1kind: PersistentVolumeClaimmetadata:name: demo-pvclaimspec:storageClassName: manualaccessModes:- ReadWriteOnceresources:requests:storage: 3Gi
kubectl create -f pvc.yaml
Verify both are bound:
kubectl get pv,pvc
Expected output:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS STORAGECLASSpersistentvolume/demo-pv 3Gi RWO Retain Bound manualNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASSpersistentvolumeclaim/demo-pvclaim Bound demo-pv 3Gi RWO manual
Create a pod that uses the PVC
apiVersion: v1kind: Podmetadata:name: task-pv-podspec:volumes:- name: pv-volumepersistentVolumeClaim:claimName: demo-pvclaimcontainers:- name: nginximage: nginxvolumeMounts:- mountPath: /usr/share/nginx/htmlname: pv-volumenodeName: <node-where-data-exists>
kubectl create -f pod.yamlkubectl get pods
Exec into the pod and verify the data:
kubectl exec -it task-pv-pod -- shcd /usr/share/nginx/htmlcat index.html
Expected output:
hello
curl localhost
Expected output:
hello
Exit and delete the pod:
kubectl delete pod task-pv-pod
Verify the PV and PVC still exist after the pod is deleted:
kubectl get pv,pvc
Both remain. Because the reclaim policy is Retain, even deleting the PVC will not delete the PV or the data on disk. These must be removed manually.
Dynamic provisioning: working example
Dynamic provisioning requires a StorageClass with a provisioner. This example uses local-path-provisioner from Rancher, which works on any cluster including Minikube and kubeadm setups.
Install local-path-provisioner
Check the latest release on GitHub and replace the version number in the command below:
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.26/deploy/local-path-storage.yaml
This creates the namespace, service account, roles, deployment, and StorageClass automatically.
Verify the StorageClass exists:
kubectl get sc
Expected output:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODElocal-path rancher.io/local-path Delete WaitForFirstConsumer
Create a StatefulSet with volumeClaimTemplates
The volumeClaimTemplates section tells Kubernetes to automatically create a PVC for each replica. The StorageClass then automatically creates a matching PV.
apiVersion: apps/v1kind: StatefulSetmetadata:name: local-testspec:replicas: 3selector:matchLabels:app: local-testtemplate:metadata:labels:app: local-testspec:containers:- name: busyboximage: busyboxcommand: ["sh", "-c", "while true; do sleep 3600; done"]volumeMounts:- mountPath: /usr/test-podname: local-volvolumeClaimTemplates:- metadata:name: local-volspec:accessModes: ["ReadWriteOnce"]storageClassName: local-pathresources:requests:storage: 128Mi
kubectl create -f statefulset.yaml
Watch the PVCs and PVs being created automatically:
kubectl get pv,pvc
Expected output once all three replicas are running:
NAME CAPACITY STATUS STORAGECLASSpersistentvolume/pvc-abc123-local-test-0 128Mi Bound local-pathpersistentvolume/pvc-def456-local-test-1 128Mi Bound local-pathpersistentvolume/pvc-ghi789-local-test-2 128Mi Bound local-pathNAME STATUS VOLUME STORAGECLASSpersistentvolumeclaim/local-vol-local-test-0 Bound pvc-abc123-local-test-0 local-pathpersistentvolumeclaim/local-vol-local-test-1 Bound pvc-def456-local-test-1 local-pathpersistentvolumeclaim/local-vol-local-test-2 Bound pvc-ghi789-local-test-2 local-path
Exec into a pod and create a file:
kubectl exec -it local-test-0 -- shcd /usr/test-podtouch testexit
Check where the pod is running:
kubectl get pods -o wide
On that node, the file will be at /opt/local-path-provisioner/. The directory name matches the PVC name.
Demonstrate the Delete reclaim policy
Scale down to 2 replicas:
kubectl edit statefulset local-test
Change replicas: 3 to replicas: 2. The pod is deleted but the PVC and PV remain:
kubectl get pv,pvc
Now delete the PVC manually:
kubectl delete persistentvolumeclaim/local-vol-local-test-2
Because the reclaim policy is Delete, the PV is deleted automatically along with the data on disk.
Static vs dynamic: when to use each
For most workloads, dynamic provisioning is the right choice. It removes the operational burden of pre-provisioning storage and scales automatically with your workloads.
For NFS-backed storage that is accessible from multiple nodes, see the NFS PersistentVolume guide.
For high-performance node-local storage, see the local volumes guide.

Marketing Team at Civo
Civo is the Sovereign Cloud and AI platform designed to help developers and enterprises build without limits. We bridge the gap between the openness of the public cloud and the rigorous security of private environments, delivering full cloud parity across every deployment. As a team, we are dedicated to providing scalable compute, lightning-fast Kubernetes, and managed services that are ready in minutes. Through CivoStack Enterprise and our FlexCore appliance, we empower organizations to maintain total data sovereignty on their own hardware.
Our mission is to make the cloud faster, simpler, and fairer. By providing enterprise-grade NVIDIA GPUs and streamlined model management, we ensure that high-performance AI and machine learning are accessible to everyone. Built for transparency and performance, the Civo Team is here to give you total control over your infrastructure, your data, and your spend.
Share this lesson