Persistent storage in Kubernetes starts with a claim that asks the cluster for a volume instead of tying a Pod to a node-local path. A PersistentVolumeClaim lets a workload request capacity and access mode from a StorageClass so application data can survive container restarts and Pod replacement.
PersistentVolumeClaim objects are namespaced. The StorageClass controls how a matching PersistentVolume is provisioned, and some classes wait until a Pod consumes the claim before binding the volume to a node.
A small lab namespace, a 1 GiB ReadWriteOnce claim, and a test Pod that writes a file through the mounted volume prove both binding and real use. Replace standard with the StorageClass used by your cluster, then keep the claim in place for real workloads instead of running the lab cleanup.
$ kubectl get storageclass NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE standard (default) rancher.io/local-path Delete WaitForFirstConsumer false 3m28s
The PVC manifest selects standard. A class with WaitForFirstConsumer can leave the claim Pending until a Pod that uses it is scheduled.
$ kubectl create namespace storage-demo namespace/storage-demo created
Use the target application namespace instead of storage-demo when creating a real claim.
Related: How to create a Kubernetes namespace
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: app-data spec: accessModes: - ReadWriteOnce storageClassName: standard resources: requests: storage: 1Gi
ReadWriteOnce allows the volume to be mounted read-write by workloads on one node at a time. Change storageClassName to the class that should provision the volume in your cluster.
$ kubectl apply -f app-data-pvc.yaml --namespace storage-demo persistentvolumeclaim/app-data created
$ kubectl get pvc app-data --namespace storage-demo NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE app-data Pending standard <unset> 0s
Pending is expected here when the selected StorageClass uses WaitForFirstConsumer. Classes with immediate binding may show Bound before a Pod exists.
apiVersion: v1 kind: Pod metadata: name: pvc-writer spec: restartPolicy: Never containers: - name: writer image: busybox:1.36 command: ["sh", "-c", "echo pvc-ok > /data/check.txt; sleep 300"] volumeMounts: - name: app-data mountPath: /data volumes: - name: app-data persistentVolumeClaim: claimName: app-data
The container writes one file to the mounted volume and stays running long enough for the readback check.
$ kubectl apply -f pvc-writer-pod.yaml --namespace storage-demo pod/pvc-writer created
$ kubectl wait --for=condition=Ready pod/pvc-writer --namespace storage-demo --timeout=120s pod/pvc-writer condition met
$ kubectl wait --for=jsonpath='{.status.phase}'=Bound pvc/app-data --namespace storage-demo --timeout=90s
persistentvolumeclaim/app-data condition met
The Bound phase means the claim has been matched with a PersistentVolume.
$ kubectl exec pvc-writer --namespace storage-demo -- cat /data/check.txt pvc-ok
$ kubectl get pvc app-data --namespace storage-demo NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE app-data Bound pvc-ae56be57-ff92-4c43-96b4-454eed635cd6 1Gi RWO standard <unset> 7s
$ kubectl delete pod pvc-writer --namespace storage-demo pod "pvc-writer" deleted from storage-demo namespace
$ kubectl delete namespace storage-demo namespace "storage-demo" deleted
Skip this command for a real application namespace. Deleting the namespace also removes the PersistentVolumeClaim and the dynamically provisioned volume.