--- title: "How to Use Persistent Volumes and Claims in Kubernetes" description: "Give Kubernetes workloads durable storage: set up a StorageClass, create PersistentVolumeClaims, use them in StatefulSets and mount a shared NFS volume." category: "Docker Kubernetes" language: en source: https://cubepath.com/docs/docker-kubernetes/persistent-volumes-in-kubernetes updated: 2026-09-25 --- # How to Use Persistent Volumes and Claims in Kubernetes Containers lose everything written to their filesystem when they are replaced. Kubernetes solves this with **PersistentVolumes** (PV), which represent a piece of storage, and **PersistentVolumeClaims** (PVC), which are requests for storage made by an application. In this tutorial you will set up dynamic provisioning with a StorageClass, attach a volume to a Pod and prove that the data survives, give each replica of a StatefulSet its own volume, and mount a shared NFS volume on several Pods. ## Prerequisites To follow this tutorial you need: - A Kubernetes cluster (1.30 or later) with at least one worker node, for example one built on CubePath VPS instances with [kubeadm on Ubuntu 24.04](/docs/docker-kubernetes/kubernetes-installation-with-kubeadm). - `kubectl` configured with cluster admin access. - For Step 5 only: an NFS server reachable from the worker nodes, exporting a directory such as `/srv/nfs/shared`. ## How storage objects fit together | Object | Who creates it | What it represents | |--------|----------------|--------------------| | PersistentVolume (PV) | An administrator, or a provisioner automatically | A real piece of storage: a disk, a directory, an NFS export | | PersistentVolumeClaim (PVC) | The application owner | "I need 5 GiB, mounted read-write by one node" | | StorageClass | An administrator | A type of storage and the provisioner that creates PVs for it on demand | When a PVC is created, Kubernetes **binds** it to a PV that satisfies it. With a StorageClass, the PV is created on the fly (dynamic provisioning); without one, an administrator must create matching PVs in advance (static provisioning). The **access mode** says how a volume can be mounted: | Access mode | Short name | Meaning | |-------------|------------|---------| | `ReadWriteOnce` | RWO | Read-write by Pods on a single node | | `ReadOnlyMany` | ROX | Read-only by many nodes | | `ReadWriteMany` | RWX | Read-write by many nodes (NFS, CephFS) | | `ReadWriteOncePod` | RWOP | Read-write by a single Pod | Block storage such as local disks only supports `ReadWriteOnce`. Use a network filesystem when several Pods on different nodes must write to the same data. ## Step 1 - Checking for a StorageClass Managed Kubernetes services normally include a default StorageClass. A cluster built with kubeadm has none: ```bash kubectl get storageclass ``` ```output No resources found ``` Without a StorageClass, any PVC stays in `Pending` forever. If your cluster already lists a class marked `(default)`, you can skip to Step 3 and use it. ## Step 2 - Installing a dynamic provisioner The Rancher [local-path-provisioner](https://github.com/rancher/local-path-provisioner) creates volumes as directories on the node where the Pod runs (under `/opt/local-path-provisioner`). It is simple and fast, and suits single-node clusters, labs and applications that replicate their own data. It does not protect against a node failure: if the node is lost, so is the volume. Install it from a tagged release manifest. Check the project's releases page for a newer version: ```bash kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.31/deploy/local-path-storage.yaml ``` Wait for the provisioner Pod: ```bash kubectl get pods -n local-path-storage ``` ```output NAME READY STATUS RESTARTS AGE local-path-provisioner-7b8c9d6f5d-kx9qz 1/1 Running 0 20s ``` Mark the new `local-path` class as the default, so PVCs without `storageClassName` use it: ```bash kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' kubectl get storageclass ``` ```output NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE local-path (default) rancher.io/local-path Delete WaitForFirstConsumer false 1m ``` Two columns matter here: - `RECLAIMPOLICY: Delete` means the volume and its data are deleted when the PVC is deleted. - `VOLUMEBINDINGMODE: WaitForFirstConsumer` means the PV is only created once a Pod uses the PVC, so it lands on the node where that Pod is scheduled. ## Step 3 - Creating a PVC and using it in a Pod Create a claim for 1 GiB and a Pod that mounts it at `/data`: ```bash nano pvc-demo.yaml ``` ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: data-demo spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi --- apiVersion: v1 kind: Pod metadata: name: writer spec: containers: - name: app image: busybox:1.36 command: ["sh", "-c", "sleep infinity"] volumeMounts: - name: data mountPath: /data volumes: - name: data persistentVolumeClaim: claimName: data-demo ``` Apply it and check the claim: ```bash kubectl apply -f pvc-demo.yaml kubectl get pvc data-demo ``` ```output NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE data-demo Bound pvc-3f1c2a9e-8b7d-4c61-9e0a-2d5b6c7e8f90 1Gi RWO local-path 15s ``` The status is `Bound`, and `kubectl get pv` shows the volume the provisioner created for it. Write a file to the volume: ```bash kubectl exec writer -- sh -c 'echo "persisted at $(date)" > /data/hello.txt' ``` Now delete the Pod, recreate it from the same file and read the data back: ```bash kubectl delete pod writer kubectl apply -f pvc-demo.yaml kubectl wait --for=condition=Ready pod/writer --timeout=60s kubectl exec writer -- cat /data/hello.txt ``` ```output persisted at Fri Sep 25 10:12:03 UTC 2026 ``` The file survived because it lives on the volume, not in the container. In a real application, reference the PVC the same way from a Deployment's Pod template. With `ReadWriteOnce`, run such a Deployment with one replica, or use a StatefulSet as shown next. ## Step 4 - Giving each StatefulSet replica its own volume Databases and other clustered services need one volume per replica that follows that replica when it is rescheduled. A StatefulSet does this with `volumeClaimTemplates`: it creates one PVC per Pod, named `