Kubernetes PersistentVolume and PVC with Examples

Tested on Rocky Linux 10.2 (Red Quartz) workstation
Package kubectl 1.36.3
Applies to Any host with kubectl configured; Kubernetes cluster with a Linux worker for the hostPath lab
Cert prep CKA · CKAD
Lab environment Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm
Privilege Normal user for kubectl commands; sudo access on worker01 is required only to remove the hostPath directory
Scope Static PersistentVolume and PersistentVolumeClaim binding, access modes, Pod consumption, label selectors, phases, reclaim policies, persistence verification, basic troubleshooting, and Deployment notes. Does not cover StorageClasses, dynamic provisioning, CSI driver administration, volume snapshots, PVC expansion, StatefulSet volumeClaimTemplates in depth, cloud backends, NFS server setup, local volume topology, or backup workflows.
Related guides Kubernetes Pods and Pod Lifecycle
Choose a Kubernetes workload resource

This walkthrough uses the pv-lab namespace. You will:

  • Create a static hostPath PersistentVolume for lab use
  • Bind a PersistentVolumeClaim
  • Mount the claim in a Pod
  • Confirm that data survives Pod deletion
IMPORTANT
This article covers static PV and PVC binding through one primary persistence lab and one optional label-selector demonstration. It does not cover StorageClasses, dynamic provisioning, or CSI driver setup. Those topics extend the same claim model when storage is provisioned on demand.

How PersistentVolumes and Claims Work

A PersistentVolume (PV) is storage made available to the cluster. A PersistentVolumeClaim (PVC) is a request for that storage. Together they work like this:

  • The PV registers storage in the cluster
  • The PVC requests size, access mode, and class
  • The Pod consumes storage through the claim name, not by naming the PV directly

Persistent storage can outlive an individual Pod. When you delete the Pod, the claim and PV normally remain until you remove them.

The relationship in one line:

Pod → PersistentVolumeClaim → PersistentVolume → storage backend

Kubernetes volume vs PersistentVolume

Pod-level volumes are defined inside Pod YAML. PersistentVolumes are separate cluster objects bound through claims.

Pod-level volume PersistentVolume
Defined directly in the Pod Separate cluster resource
Often tied to Pod lifetime Exists independently of Pods
Suitable for temporary or configuration data Suitable for persistent application data
Examples: emptyDir, ConfigMap, Secret Consumed through a PVC

For emptyDir, ConfigMap, Secret, projected, and hostPath mounts, see Kubernetes volumes. This article focuses on PV and PVC binding and consumption.

PersistentVolume and PVC YAML

A static PV declares capacity, access modes, reclaim policy, and a storage source. For this lab:

  • hostPath maps to a directory on a single node
  • Kubernetes documents hostPath PVs as suitable for single-node testing
  • PV node affinity constrains Pods to the node that can access the storage

This lab pins the PV to worker01 so every Pod using the claim reaches the same node directory. Run kubectl get nodes and replace worker01 if your worker uses another hostname.

Save this manifest as lab-pv.yaml:

yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: lab-pv
  labels:
    type: local
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - worker01
  hostPath:
    path: /tmp/pv-lab-data
    type: DirectoryOrCreate

Key PV fields:

  • spec.capacity.storage — size Kubernetes advertises for binding
  • spec.accessModes — supported mount modes (see the access mode table below)
  • spec.persistentVolumeReclaimPolicy — what happens after the claim is deleted
  • spec.storageClassName — must match the claim when you use static binding with a named class
  • spec.nodeAffinity — limits which node can run Pods that use this volume
  • spec.hostPath.path — node directory used in this lab (not a production pattern)

For this hostPath lab, capacity.storage: 1Gi:

  • Participates in PV/PVC matching
  • Does not impose a 1 GiB filesystem quota
  • Does not count toward Pod ephemeral-storage consumption

A hostPath directory can still consume more node disk space. Monitor and clean the node path separately.

Save this manifest as lab-pvc.yaml:

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: lab-pvc
  namespace: pv-lab
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi
  storageClassName: manual

Key PVC fields:

  • spec.accessModes — mount mode the claim requests
  • spec.resources.requests.storage — minimum size the binder must satisfy
  • spec.storageClassName — must match the PV when both objects set a class name

The claim must match an available PV:

  • Requested storage and access mode must be compatible
  • A claim can bind to a larger PV, but not to one smaller than its request
  • storageClassName: manual on both objects avoids default StorageClass dynamic provisioning during this lab

Access modes

Access modes describe storage capabilities and binding requirements. Support depends on the underlying storage system.

Access mode Meaning
ReadWriteOnce (RWO) Read-write mount from a single node
ReadOnlyMany (ROX) Read-only mount from multiple nodes
ReadWriteMany (RWX) Read-write mount from multiple nodes
ReadWriteOncePod (RWOP) Read-write mount by one Pod

ReadWriteOncePod notes for this lab:

  • Supported only for CSI volumes
  • Cannot be used with the hostPath PV in this walkthrough
  • Became stable in Kubernetes v1.29, but remains CSI-only

Access modes define what the volume supports and what the scheduler and attach logic can allow. They do not automatically enforce write protection after a volume is mounted in every scenario.


Create and Bind a Static PV and PVC

Follow this binding sequence:

  • Create the pv-lab namespace
  • Register the PV and wait until it is Available
  • Create the PVC and wait until it is Bound
  • Inspect both objects to confirm the binding

Create the lab namespace first:

bash
kubectl create namespace pv-lab

Sample output:

output
namespace/pv-lab created

Apply the PersistentVolume:

bash
kubectl apply -f lab-pv.yaml

Sample output:

output
persistentvolume/lab-pv created

Wait for the PV:

bash
kubectl wait --for=jsonpath='{.status.phase}'=Available pv/lab-pv --timeout=30s

Sample output:

output
persistentvolume/lab-pv condition met

Confirm the PV is Available:

bash
kubectl get pv lab-pv

Sample output:

output
NAME     CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
lab-pv   1Gi        RWO            Retain           Available           manual         <unset>                          0s

Available means the PV is registered and waiting for a matching claim.

Apply the PersistentVolumeClaim:

bash
kubectl apply -f lab-pvc.yaml

Sample output:

output
persistentvolumeclaim/lab-pvc created

Wait for binding before displaying Bound output:

bash
kubectl wait --for=jsonpath='{.status.phase}'=Bound pvc/lab-pvc -n pv-lab --timeout=60s

Sample output:

output
persistentvolumeclaim/lab-pvc condition met

List the bound claim:

bash
kubectl get pvc -n pv-lab

Sample output:

output
NAME      STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
lab-pvc   Bound    lab-pv   1Gi        RWO            manual         <unset>                 2s

The claim moved from Pending to Bound once the binder found lab-pv.

Confirm the PV is now Bound to the claim:

bash
kubectl get pv lab-pv

Sample output:

output
NAME     CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM            STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
lab-pv   1Gi        RWO            Retain           Bound    pv-lab/lab-pvc   manual         <unset>                          3s

The PV phase is now Bound and the CLAIM column shows pv-lab/lab-pvc.

Inspect the bound claim:

bash
kubectl describe pvc lab-pvc -n pv-lab

Sample output:

output
Name:          lab-pvc
Namespace:     pv-lab
StorageClass:  manual
Status:        Bound
Volume:        lab-pv
Capacity:      1Gi
Access Modes:  RWO
VolumeMode:    Filesystem

Volume: lab-pv confirms which PV satisfied the claim.

Inspect the PV side:

bash
kubectl describe pv lab-pv

Sample output:

output
Name:            lab-pv
Status:          Bound
Claim:           pv-lab/lab-pvc
Reclaim Policy:  Retain
Access Modes:    RWO
Capacity:        1Gi
Source:
    Type:          HostPath (bare host directory volume)
    Path:          /tmp/pv-lab-data

How Kubernetes matches claims and volumes

The binder compares the claim against Available PVs. Main requirements:

  • Requested storage size — PV capacity must be at least the claim request
  • Compatible access mode — the PV must support the mode the claim requests
  • storageClassName — must match when both objects set a class name
  • Volume mode — Filesystem versus Block must align
  • Optional label selector — claim can require PV labels
  • PV availability — phase must be Available (not Bound or Released)

A claim can bind to a larger compatible PV, but not to one smaller than its request. This article does not cover internal control-plane binding implementation detail.


Mount the PVC and Verify Persistence

The Pod references the claim name, not the PV name:

  • claimName in the Pod must match the PVC in the same namespace
  • The scheduler applies the bound PV's node affinity to Pods using that claim
  • Pod manifests do not need their own nodeSelector for this lab

Write data through the claim

Save pv-writer.yaml:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: pv-writer
  namespace: pv-lab
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "echo lab-data-2026 > /data/persist.txt && sleep 3600"]
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: lab-pvc

spec.volumes[].persistentVolumeClaim.claimName must match the PVC name in the same namespace.

Apply the writer Pod:

bash
kubectl apply -f pv-writer.yaml

Wait until the Pod is ready and the volume is mounted:

bash
kubectl wait --for=condition=Ready pod/pv-writer -n pv-lab --timeout=90s

The Pod is ready when the volume is bound and mounted.

Verify the mounted file exists:

bash
kubectl exec pv-writer -n pv-lab -- sh -c 'until [ -s /data/persist.txt ]; do sleep 1; done; cat /data/persist.txt'

Sample output:

output
lab-data-2026

The file was written on the mounted PVC path inside the container.

Recreate the Pod and read the data

Persistence depends on the PV and underlying storage, not on the container filesystem layer. To prove that:

  • Delete the writer Pod but keep the PVC
  • Create a replacement Pod that mounts the same claim
  • Read the file from the mounted path with kubectl exec
bash
kubectl delete pod pv-writer -n pv-lab --wait=true

The PVC and PV remain; only the Pod object is removed.

Create a reader Pod that only reads the file. Save pv-reader.yaml:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: pv-reader
  namespace: pv-lab
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: lab-pvc

Apply the reader Pod and wait until it is ready:

bash
kubectl apply -f pv-reader.yaml
bash
kubectl wait --for=condition=Ready pod/pv-reader -n pv-lab --timeout=90s

Read the persisted file from the replacement Pod:

bash
kubectl exec pv-reader -n pv-lab -- sh -c 'until [ -s /data/persist.txt ]; do sleep 1; done; cat /data/persist.txt'

Sample output:

output
lab-data-2026

The same content appears after Pod replacement because the data lives on the bound volume behind the claim.

Delete the reader Pod when finished:

bash
kubectl delete pod pv-reader -n pv-lab --ignore-not-found=true

Select a Static PV Using Labels

When several static PVs match size and class:

  • Add labels on the PV
  • Add a selector on the PVC to pick a specific volume

This demonstration can coexist with the main lab because:

  • Selector resources use different names
  • gold-pvc excludes lab-pv by requiring tier: gold

Both selector PVs use the same worker01 node affinity as lab-pv.

Create two PVs with different tier labels. Save both PV definitions as selector-pvs.yaml:

yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-gold
  labels:
    tier: gold
spec:
  capacity:
    storage: 2Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - worker01
  hostPath:
    path: /tmp/pv-gold
    type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-silver
  labels:
    tier: silver
spec:
  capacity:
    storage: 2Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - worker01
  hostPath:
    path: /tmp/pv-silver
    type: DirectoryOrCreate

Register both selector PVs:

bash
kubectl apply -f selector-pvs.yaml

Sample output:

output
persistentvolume/pv-gold created
persistentvolume/pv-silver created

Apply the labeled claim as gold-pvc.yaml:

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: gold-pvc
  namespace: pv-lab
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi
  storageClassName: manual
  selector:
    matchLabels:
      tier: gold

Apply the claim:

bash
kubectl apply -f gold-pvc.yaml

Wait for binding before displaying Bound output:

bash
kubectl wait --for=jsonpath='{.status.phase}'=Bound pvc/gold-pvc -n pv-lab --timeout=60s

Sample output:

output
persistentvolumeclaim/gold-pvc condition met

List the bound selector claim:

bash
kubectl get pvc gold-pvc -n pv-lab

Sample output:

output
NAME       STATUS   VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
gold-pvc   Bound    pv-gold   2Gi        RWO            manual         <unset>                 2s

Compare both selector PVs:

bash
kubectl get pv pv-gold pv-silver

Sample output:

output
NAME        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM             STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pv-gold     2Gi        RWO            Retain           Bound       pv-lab/gold-pvc   manual         <unset>                          85s
pv-silver   2Gi        RWO            Retain           Available                     manual         <unset>                          85s

pv-gold bound to the claim; pv-silver stayed Available because the selector excluded it. For general selector syntax, see Kubernetes labels and selectors.

To demonstrate Released with Retain:

  • Delete the selector claim
  • Wait until the bound PV reports Released
  • Inspect the PV before deleting the selector PV objects

Remove the claim first:

bash
kubectl delete pvc gold-pvc -n pv-lab --wait=true

Wait until the bound PV reports Released:

bash
kubectl wait --for=jsonpath='{.status.phase}'=Released pv/pv-gold --timeout=30s

Sample output:

output
persistentvolume/pv-gold condition met

Inspect the Released PV:

bash
kubectl get pv pv-gold

Sample output:

output
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS     CLAIM             STORAGECLASS   AGE
pv-gold   2Gi        RWO            Retain           Released   pv-lab/gold-pvc   manual         85s

A PV using Retain:

  • Moves to Released after the claim is deleted
  • Stays unavailable for another claim until it is manually reclaimed

Then delete the selector PVs:

bash
kubectl delete pv pv-gold pv-silver

PV and PVC Lifecycle

Phases describe binding and lifecycle state. They are separate from Pod STATUS.

PV and PVC phases

PersistentVolume phases

Phase Meaning
Pending The PV object is being initialized and is not yet available for binding
Available Not yet bound to a claim
Bound Bound to a PVC
Released Claim was deleted; storage has not been reclaimed
Failed An automatic reclamation operation failed

New PVs initially enter Pending, although the transition to Available is often too quick to notice with kubectl get.

PersistentVolumeClaim phases

Phase Meaning
Pending No compatible PV has been bound yet
Bound Claim is bound to a PV
Lost Previously bound volume is no longer available

A new claim may show Pending until a matching PV exists or the binder completes matching. After you apply a compatible PV, expect:

  • PendingBound on the claim
  • AvailableBound on the volume

Reclaim policies

Reclaim policy applies to the PV and controls what happens after the bound PVC is deleted.

Policy Behaviour after PVC deletion
Retain PV and underlying data are preserved for manual handling
Delete PV and supported underlying storage are deleted

Recycle is obsolete and should not be used.

With Retain, deleting the claim leaves the PV in Released. The selector demo above demonstrated this transition on pv-gold before those PV objects were deleted. To reuse a retained volume, you normally:

  • Delete the PV object
  • Clean the backend data
  • Create a new PV before binding another claim

Backend-specific cleanup and PV reuse are outside this article.

Delete and clean up storage

Each delete step has a different effect:

  • Delete the Pod — does not remove the PVC or PV; the claim stays Bound and the data remains on the volume
  • Delete the PVC — with Retain, the PV moves to Released and data remains until an administrator handles it; with Delete, Kubernetes may remove the PV and underlying storage when the provisioner supports deletion
  • Delete the PV — removes the Kubernetes record; effect on underlying files depends on volume type and whether a claim still references the volume

Troubleshoot a PVC Stuck in Pending

Start troubleshooting with:

  • kubectl describe pvc for claim Events
  • kubectl get pv for available volumes
bash
kubectl describe pvc <claim-name> -n <namespace>

List available PVs:

bash
kubectl get pv
Symptom Likely cause Fix
PVC Pending, no matching PV No Available PV with enough capacity Create or free a compatible PV
PVC Pending, requested size exceeds PV capacity No compatible PV is large enough Reduce the claim request or create a larger PV
PVC Pending, access mode mismatch PV does not support requested mode Align accessModes on PV and PVC
PVC Pending, class mismatch storageClassName differs between PV and PVC Set the same class on both or use matching empty string for static binding
PVC Pending, selector mismatch No PV carries required labels Add labels to the intended PV or relax the selector
PVC Pending, volume mode mismatch Filesystem versus Block differ Match volumeMode on both objects
Pod Pending after PVC is Bound No schedulable node satisfies the PV node affinity, or the pinned node is unavailable or unschedulable Check Pod Events; confirm worker01 is Ready, schedulable, and its kubernetes.io/hostname label matches the PV
Pod mount error, PVC not found Pod and PVC in different namespaces Create the claim in the Pod namespace or reference the correct name
PV Released after PVC deletion Expected with Retain Manually reclaim the storage; normally delete the PV object, clean the backend data, and create a new PV before reuse

Use PVCs with Deployments

A Deployment Pod can mount a PVC using the same pattern as a standalone Pod:

  • persistentVolumeClaim under spec.volumes
  • Matching volumeMounts on the container

Sharing one claim across replicas works only when:

  • The storage backend supports the attachment pattern
  • The access mode allows the intended number of node attachments
  • You do not assume one ReadWriteOnce claim safely backs many replicas spread across nodes

For per-replica storage identity and ordered rollout with claims, see Kubernetes StatefulSets.


What's Next


References


Summary

You registered a static PersistentVolume, confirmed it was Available, created a matching PersistentVolumeClaim, and then confirmed that both objects became Bound. The Pod mounted the claim by name through persistentVolumeClaim.claimName, and data written to the mount path survived Pod deletion because it lives on the volume behind the claim, not in the container image layer.

Access modes and reclaim policy matter as much as YAML shape:

  • ReadWriteOnce permits read-write mounting from a single node
  • Retain leaves a Released PV after claim deletion until an administrator handles reuse
  • Label selectors let you pick a specific static PV when several compatible volumes exist

For Pod-level scratch and configuration mounts, use Pod volumes instead of PV objects. When replicas need stable storage identity or ordered binding, move to StatefulSet patterns. StorageClasses and dynamic provisioning build on the same PVC model for production clusters.


Frequently Asked Questions

1. What is the difference between a PersistentVolume and a PersistentVolumeClaim?

A PersistentVolume is cluster storage registered by an administrator or provisioner. A PersistentVolumeClaim is a user request for storage. Pods consume storage through the claim, not by referencing the PV object directly.

2. Why is my PVC stuck in Pending?

Pending usually means no compatible PV is Available. Check requested size, access modes, storageClassName, volume mode, label selectors, and Events from kubectl describe pvc. A claim can also stay Pending briefly while the binder matches a new PV.

3. Can a Pod mount a PersistentVolume directly?

No. Pods reference a PersistentVolumeClaim name under spec.volumes.persistentVolumeClaim.claimName. Kubernetes binds the claim to a matching PV before the volume is mounted.

4. What happens when I delete a PVC with reclaim policy Retain?

The PV moves to Released. Underlying data remains on the backend, but the volume is not automatically available for a new claim until an administrator clears or reclaims it according to storage type.

5. Can multiple Pods share one ReadWriteOnce PVC?

ReadWriteOnce allows read-write mount from a single node. Multiple Pods on the same node may share the volume when the storage driver allows it, but you should not assume one RWO claim safely backs many replicas spread across nodes.

6. Does deleting a PVC always delete my data?

It depends on the PV reclaim policy. Retain keeps the PV object and backend data for manual handling. Delete removes the PV and may remove supported underlying storage. Recycle is obsolete and should not be used.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)