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

4 minutes reading time

Written by

Civo Team
Civo Team

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.

pv-pvc-binding-lifecycle

Access modes

ModeShort nameWhat it means

ReadWriteOnce

RWO

Mounted read-write by a single node

ReadOnlyMany

ROX

Mounted read-only by many nodes simultaneously

ReadWriteMany

RWX

Mounted read-write by many nodes simultaneously

Not all storage backends support all access modes. Check your storage provider's documentation.

Reclaim policies

PolicyWhat happens when the PVC is deleted

Retain

PV stays in Released status. Data is preserved but the PV must be manually reclaimed before reuse.

Delete

PV and its underlying storage are deleted automatically. Default for most dynamically provisioned storage.

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/data
echo "hello" > /mnt/data/index.html
apiVersion: v1
kind: PersistentVolume
metadata:
name: demo-pv
spec:
storageClassName: manual
capacity:
storage: 3Gi
accessModes:
- ReadWriteOnce
hostPath:
path: /mnt/data
persistentVolumeReclaimPolicy: Retain
kubectl create -f pv.yaml

Create the PersistentVolumeClaim

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: demo-pvclaim
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
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 STORAGECLASS
persistentvolume/demo-pv 3Gi RWO Retain Bound manual
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
persistentvolumeclaim/demo-pvclaim Bound demo-pv 3Gi RWO manual

Create a pod that uses the PVC

apiVersion: v1
kind: Pod
metadata:
name: task-pv-pod
spec:
volumes:
- name: pv-volume
persistentVolumeClaim:
claimName: demo-pvclaim
containers:
- name: nginx
image: nginx
volumeMounts:
- mountPath: /usr/share/nginx/html
name: pv-volume
nodeName: <node-where-data-exists>
kubectl create -f pod.yaml
kubectl get pods

Exec into the pod and verify the data:

kubectl exec -it task-pv-pod -- sh
cd /usr/share/nginx/html
cat 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 VOLUMEBINDINGMODE
local-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/v1
kind: StatefulSet
metadata:
name: local-test
spec:
replicas: 3
selector:
matchLabels:
app: local-test
template:
metadata:
labels:
app: local-test
spec:
containers:
- name: busybox
image: busybox
command: ["sh", "-c", "while true; do sleep 3600; done"]
volumeMounts:
- mountPath: /usr/test-pod
name: local-vol
volumeClaimTemplates:
- metadata:
name: local-vol
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: local-path
resources:
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 STORAGECLASS
persistentvolume/pvc-abc123-local-test-0 128Mi Bound local-path
persistentvolume/pvc-def456-local-test-1 128Mi Bound local-path
persistentvolume/pvc-ghi789-local-test-2 128Mi Bound local-path
NAME STATUS VOLUME STORAGECLASS
persistentvolumeclaim/local-vol-local-test-0 Bound pvc-abc123-local-test-0 local-path
persistentvolumeclaim/local-vol-local-test-1 Bound pvc-def456-local-test-1 local-path
persistentvolumeclaim/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 -- sh
cd /usr/test-pod
touch test
exit

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

Static provisioningDynamic provisioning

Who creates the PV

Admin manually

StorageClass automatically

When to use

Pre-allocated storage, specific node or hardware requirements

Self-service workloads, cloud environments, most production cases

Requires StorageClass

No (or manual)

Yes

Scales easily

No

Yes

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.

Civo Team
Civo Team

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.

View author profile