Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Kubernetes Persistent Volumes and Storage

By Kokil Thapa | Last reviewed: September 2026

Stateful workloads break the moment you treat pods like cattle and disks like scratch space. Kubernetes Persistent Volumes and Storage exist so databases, upload directories, and queue data survive rescheduling, node drains, and rolling updates. If you run Laravel queues, document portals, or eCommerce carts on Kubernetes, you need a clear model for how data outlives any single container. This guide walks through PVs, PVCs, StorageClasses, CSI drivers, access modes, and the production mistakes I see on real clusters.

What are Kubernetes Persistent Volumes and Storage in plain terms?

Pods are ephemeral. Their root filesystem disappears when the pod is deleted. Docker volumes solve this on a single host, but Kubernetes schedules pods across many nodes. You need cluster-level storage objects that survive pod lifecycle events.

Three objects form the core contract:

  • PersistentVolume (PV) — a piece of storage in the cluster, like a 50 GiB block volume or NFS export.
  • PersistentVolumeClaim (PVC) — a pod's request for storage with size, access mode, and optional StorageClass.
  • StorageClass — a template that tells a provisioner how to create PVs dynamically.

Think of PV as the inventory shelf, PVC as the order form, and StorageClass as the factory line that builds new shelves on demand. The Kubernetes control plane binds a PVC to a matching PV, then kubelet mounts it into the pod spec.

PV, PVC, and Pod Mount FlowStorageClassProvisioner rulesPersistentVolume50 GiB blockPVCApp requestPodvolumeMountBackend: cloud disk, NFS, Ceph, OpenEBS, local SSDData survives pod delete and reschedule
Kubernetes Persistent Volumes and Storage: StorageClass provisions PV, PVC binds, Pod mounts durable data.

How do you create a PersistentVolumeClaim and mount it in a pod?

Most teams use dynamic provisioning. You define a StorageClass once, then every PVC triggers automatic PV creation. Static provisioning still matters when you attach pre-existing NFS exports or legacy SAN LUNs.

Step 1: Define a StorageClass

Cloud clusters usually ship with a default class. For AWS EBS, GCE PD, or DigitalOcean block storage, confirm the provisioner name matches your CSI driver. A minimal StorageClass for a cloud disk might look like this:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  fsType: ext4
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true

WaitForFirstConsumer delays binding until Kubernetes knows which node will run the pod. That prevents provisioning a volume in the wrong availability zone. For zone-aware clusters, this setting saves hours of debugging.

Step 2: Create the PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data
  namespace: production
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: fast-ssd
  resources:
    requests:
      storage: 20Gi

The PVC stays in Pending until a matching PV exists or the provisioner creates one. Check status with kubectl get pvc app-data -n production. The VOLUME column shows the bound PV name once ready.

Step 3: Mount the PVC in a Deployment or StatefulSet

apiVersion: apps/v1
kind: Deployment
metadata:
  name: laravel-api
spec:
  replicas: 1
  template:
    spec:
      containers:
        - name: php-fpm
          image: myregistry/laravel-api:2026.09
          volumeMounts:
            - name: storage
              mountPath: /var/www/storage/app
      volumes:
        - name: storage
          persistentVolumeClaim:
            claimName: app-data

On a production Laravel application, mount storage/app for uploads and keep the database on a separate PVC. Never share one ReadWriteOnce volume across multiple replicas unless your app handles file locking. That pattern breaks under load.

Dynamic PVC Binding Sequence1. Apply PVCPending state2. Provision PVCSI creates disk3. Bind PVCBound status4. Schedule PodNode selected5. Kubelet attaches volume to nodeMount published to pod spec6. Container starts with durable mountPathData persists across restartsFailure point: wrong zone or missing CSI driver
Dynamic provisioning flow: PVC triggers PV creation, binding, scheduling, attach, and mount.

Which access modes and reclaim policies should you choose?

Access modes define how many nodes can mount a volume simultaneously. Reclaim policies define what happens to the PV after the PVC is deleted. Getting either wrong causes data loss or stuck volumes.

Access modeMeaningTypical use
ReadWriteOnce (RWO)Single node read/writeMySQL, PostgreSQL, single-replica app storage
ReadOnlyMany (ROX)Many nodes read-onlyStatic assets, config bundles on shared NFS
ReadWriteMany (RWX)Many nodes read/writeShared uploads, NFS-backed Laravel storage
ReadWriteOncePod (RWOP)Single pod read/writeStrict pod-level isolation on supported CSI drivers

Reclaim policies matter for cost and safety:

  • Delete — the PV and underlying cloud disk are removed when the PVC is deleted. Fine for dev. Dangerous for production databases without backups.
  • Retain — the PV object stays, and the admin must manually reclaim the disk. Safer for production when paired with snapshots.

I prefer Retain on production database StorageClasses. Pair it with volume snapshots and Velero cluster backups. A mistaken kubectl delete pvc should not wipe five years of orders.

What storage backends work best for Kubernetes Persistent Volumes and Storage?

Your backend choice depends on access mode needs, performance, and operational budget. Cloud block storage is the default path. Shared file systems enter when multiple pods need the same directory.

BackendAccessProsCons
Cloud block (EBS, PD, DO Volume)RWOFast, managed, simple CSIZone-locked, not shared across nodes
NFSRWXShared files, familiar opsLatency, single-server bottleneck
Ceph RBD / CephFSRWO / RWXScale-out, snapshotsHeavy ops, needs tuning
OpenEBSRWOK8s-native on bare metalReplica overhead on small clusters
GlusterFSRWXDistributed file layerDeclining community momentum
Local SSD (local-path)RWOLow latencyNot portable across nodes

For a booking platform like Adventure Third Pole Trek, block storage on a single-replica StatefulSet handles MySQL well. Shared RWX storage fits document upload directories when you scale PHP-FPM horizontally. Match the backend to the workload, not the other way around.

Modern clusters use the Container Storage Interface (CSI) instead of in-tree plugins. Install the vendor CSI driver, deploy its controller and node DaemonSets, then reference the driver in StorageClass provisioner fields. The official Persistent Volumes documentation remains the authoritative reference for object fields and status phases.

Storage Backend Decision TreeNeed shared files?No: use blockRWO cloud diskYes: use fileRWX NFS or CephFSHA database?StatefulSet + RWOProduction default: block for DB, RWX only when app requires itAlways set reclaimPolicy Retain on prod data classes
Choose block storage for databases, shared file storage only when multiple pods need the same path.

How do StatefulSets differ from Deployments for persistent storage?

Deployments treat pods as interchangeable. StatefulSets give each pod a stable network identity and a dedicated PVC through volumeClaimTemplates. That pattern suits MySQL replicas, Redis, Elasticsearch, and Kafka.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
        - name: mysql
          image: mysql:9.7
          volumeMounts:
            - name: data
              mountPath: /var/lib/mysql
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: fast-ssd
        resources:
          requests:
            storage: 100Gi

StatefulSet pod names are predictable: mysql-0, mysql-1, mysql-2. Each gets its own PVC named data-mysql-0 and so on. Deleting the StatefulSet does not delete those PVCs by default. That protects data but can leave orphaned disks billing your cloud account.

For Laravel on Kubernetes, keep the web tier on Deployments with object storage (S3-compatible) for uploads when possible. Reach for StatefulSets when you self-host MySQL or Redis inside the cluster. Managed databases outside the cluster simplify ops for small teams.

What production mistakes break Kubernetes Persistent Volumes and Storage?

Storage failures rarely announce themselves during deployment. They surface at 2 a.m. when a pod reschedules to another node and never mounts.

Common failure patterns

  1. Multi-replica Deployment + RWO volume — only one pod mounts; others stay Pending. Use RWX or external object storage.
  2. Wrong availability zone — volume provisioned in zone A, pod scheduled in zone B. Use WaitForFirstConsumer.
  3. Missing CSI node plugin — controller creates disk but kubelet cannot attach. Check DaemonSet on the target node.
  4. Delete reclaim on production — PVC deletion wipes the cloud disk. Switch to Retain and automate snapshot cleanup.
  5. No backup path — PV redundancy is not backup. Schedule VolumeSnapshot objects or Velero jobs.
  6. Running out of inode or disk space — monitor used bytes and inodes. Expand PVCs if allowVolumeExpansion: true is set.

When a pod enters ContainerCreating forever, describe the pod and check events. Messages like FailedAttachVolume or FailedMount point to attach or filesystem issues. The CrashLoopBackOff debugging guide covers related scheduling problems.

Production Storage GotchasZone mismatchPod and disk differRWO + replicasOnly one pod mountsDelete policyPVC delete wipes dataFix: WaitForFirstConsumer + Retain + snapshotsValidate with kubectl describe pvc and pod eventsMonitor: kubelet volume stats, cloud disk metricsAlert before PVC reaches 85% capacity
Zone mismatch, access mode errors, and Delete reclaim policies cause most production storage incidents.

Expanding a PVC without downtime

When StorageClass allows expansion, patch the PVC request and restart the pod if the filesystem does not auto-resize:

kubectl patch pvc app-data -n production \
  -p '{"spec":{"resources":{"requests":{"storage":"40Gi"}}}}'

kubectl exec -it deploy/laravel-api -- df -h /var/www/storage/app

Not every CSI driver supports online expansion. Test in staging first. Document the steps in your runbook alongside PV lifecycle management.

Security and performance notes

Encrypt cloud disks at rest through StorageClass parameters. Use fsGroup in pod security context so non-root containers can write to mounted volumes. For sensitive legal-tech document stores, combine encrypted PVs with Secrets management for credentials.

Performance tuning starts with disk type and IOPS limits. A Rs 3,000/month (~USD 22) standard disk will bottleneck a busy MySQL instance. Match disk tier to query load. Review cluster performance tuning alongside storage metrics.

I've seen teams compare SAN versus NAS architectures before choosing in-cluster backends. Block SAN behaviour maps cleanly to RWO cloud volumes. NAS file shares map to NFS or CephFS when you need RWX.

Key Takeaways

  • Bind durable data through PV + PVC; let StorageClass and CSI handle dynamic provisioning.
  • Use RWO block storage for databases; use RWX file storage only when multiple pods share files.
  • Set reclaimPolicy: Retain and snapshot production volumes before any PVC deletion.
  • Prefer WaitForFirstConsumer on zone-aware clusters to avoid attach failures.
  • StatefulSets with volumeClaimTemplates give each replica its own PVC; Deployments do not.
  • PV redundancy is not backup — schedule Velero or VolumeSnapshot jobs and test restores.

People Also Ask

What is the difference between a PersistentVolume and a PersistentVolumeClaim?

A PersistentVolume is cluster storage inventory — the actual disk or export. A PersistentVolumeClaim is an application's request for storage with size and access requirements. Kubernetes binds one PVC to one PV when capacity, mode, and StorageClass match.

Can multiple pods share the same PersistentVolumeClaim?

Only when the volume supports ReadWriteMany or ReadWriteOncePod and the storage backend allows concurrent mounts. Standard cloud block volumes use ReadWriteOnce, so a single node mounts them. For shared Laravel upload folders, use NFS, CephFS, or object storage instead.

What happens to data when you delete a PersistentVolumeClaim?

It depends on the StorageClass reclaim policy. With Delete, the underlying cloud disk is destroyed. With Retain, the PV remains in Released state and an admin must manually recover or rebind the disk. Production clusters should use Retain plus automated snapshots.

Do you need StatefulSets for persistent storage?

Not always. A Deployment with a single replica and one PVC works for simple stateful apps. StatefulSets add stable pod names, ordered rollout, and per-pod PVC templates — essential for clustered databases and distributed systems that rely on identity.

Build stateful workloads that survive real operations

Kubernetes Persistent Volumes and Storage are not optional once your application holds user uploads, orders, or case files. Define StorageClasses with sane reclaim policies, match access modes to replica counts, install CSI drivers correctly, and backup outside the cluster lifecycle. If you are moving a Laravel app, legal portal, or eCommerce platform onto Kubernetes and want storage designed for production from day one, enterprise application development and Linux system administration cover architecture through ongoing ops. Need help validating YAML before apply? Use the JSON formatter to inspect rendered manifests, then contact us to review your cluster storage design.

Frequently Asked Questions

They decouple durable disk from pods: admins or provisioners create PersistentVolumes, apps request PersistentVolumeClaims, and the scheduler mounts bound storage so data survives restarts and rescheduling.

A PersistentVolume is cluster storage inventory — the actual block volume, NFS export, or cloud disk. A PersistentVolumeClaim is the application’s order form specifying size, access mode, and optional StorageClass. Kubernetes binds one PVC to one matching PV when capacity, access mode, and StorageClass align. The kubelet then mounts that volume into the pod spec. Think of PV as the shelf, PVC as the request, and binding as matching inventory to demand before the container starts.

Most teams use dynamic provisioning. Define a StorageClass once with the correct CSI provisioner, then create a PVC requesting storage size and access mode. Check binding with kubectl get pvc app-data -n production — the VOLUME column shows the bound PV name. In your Deployment or StatefulSet, reference the PVC under volumes and set volumeMounts with the container path. On a Laravel API, mount storage/app for uploads. Keep the database on a separate PVC. Never attach one ReadWriteOnce volume to multiple replicas unless your app handles file locking.

A StorageClass is the factory template that tells a provisioner how to create PersistentVolumes on demand. You define the CSI provisioner name, disk parameters like type gp3 and fsType ext4, reclaimPolicy, volumeBindingMode, and allowVolumeExpansion. Cloud clusters often ship with a default class for EBS, GCE PD, or DigitalOcean block storage. Every PVC referencing that class triggers automatic PV creation. Static provisioning still applies when attaching pre-existing NFS exports or legacy SAN LUNs that you define as PV objects manually.

ReadWriteOnce suits single-node databases like MySQL and single-replica app storage. ReadOnlyMany fits static assets on shared NFS. ReadWriteMany is for shared upload directories when PHP-FPM scales horizontally. ReadWriteOncePod gives strict pod-level isolation on supported CSI drivers. For reclaim policies, Delete removes the cloud disk when the PVC is deleted — fine for dev, dangerous for production databases. Retain keeps the PV in Released state for manual recovery. Pair Retain with volume snapshots and Velero cluster backups on production StorageClasses.

Use Retain, not Delete. A mistaken kubectl delete pvc should not wipe years of orders or case files.

Cloud block storage — EBS, GCE PD, DigitalOcean Volume — is the default for ReadWriteOnce workloads: fast, managed, and simple via CSI, but zone-locked and not shared across nodes. NFS gives ReadWriteMany for shared files with familiar ops, though latency and single-server bottlenecks hurt under load. Ceph RBD and CephFS scale out with snapshots but need heavy tuning. OpenEBS suits bare-metal clusters. Local SSD via local-path offers low latency but is not portable. Match backend to workload: block for databases, shared file storage only when multiple pods need the same path.

WaitForFirstConsumer delays PVC binding until Kubernetes knows which node will run the pod. Without it, a provisioner may create a volume in availability zone A while the scheduler places the pod in zone B. The pod then enters ContainerCreating with FailedAttachVolume events and never mounts. On zone-aware cloud clusters, this setting prevents hours of debugging. Combine it with the correct CSI driver provisioner name in your StorageClass. Describe stuck pods and check events when attach failures appear during rescheduling or node drains.

Deployments treat pods as interchangeable cattle. StatefulSets give each pod a stable network identity and a dedicated PVC through volumeClaimTemplates. Pod names like mysql-0, mysql-1 are predictable, and each gets its own PVC such as data-mysql-0. That pattern suits MySQL replicas, Redis, Elasticsearch, and Kafka. Deleting the StatefulSet does not delete PVCs by default — protecting data but leaving orphaned disks on your cloud bill. For Laravel, keep the web tier on Deployments with S3-compatible object storage when possible. Reach for StatefulSets when self-hosting MySQL or Redis inside the cluster.

Only when the volume supports ReadWriteMany or ReadWriteOncePod and the storage backend allows concurrent mounts. Standard cloud block volumes use ReadWriteOnce, meaning a single node mounts them at one time. A multi-replica Deployment with one RWO PVC leaves extra pods Pending — a common production mistake. For shared Laravel upload folders across PHP-FPM replicas, use NFS, CephFS, or external object storage instead of block storage. ReadOnlyMany works for static assets many pods read but none write. Match access mode to replica count before you deploy.

Outcome depends on the StorageClass reclaimPolicy. With Delete, Kubernetes removes the PV and destroys the underlying cloud disk — wiping data immediately. With Retain, the PV enters Released state and an admin must manually reclaim, rebind, or snapshot the disk before reuse. Production clusters should use Retain plus automated VolumeSnapshot objects or Velero jobs. PV redundancy is not backup. Schedule snapshot and restore tests in your runbook so a routine PVC deletion during a bad deploy does not become permanent data loss.

Not always. A Deployment with a single replica and one PVC works for simple stateful apps like a lone Laravel worker or single-replica API with mounted uploads. StatefulSets add stable pod names, ordered rollout, and per-pod PVC templates — essential for clustered databases and distributed systems that rely on identity. Choose StatefulSets for MySQL, Redis, or Elasticsearch replicas inside the cluster. Managed databases outside the cluster simplify ops for small teams that lack storage administration capacity. The decision is about pod identity and replica topology, not storage existence alone.

Common failures surface at 2 a.m. when pods reschedule. Multi-replica Deployments sharing one ReadWriteOnce volume leave pods Pending. Wrong availability zones cause attach failures — fix with WaitForFirstConsumer. Missing CSI node DaemonSets mean the controller creates disks but kubelet cannot attach. Delete reclaim on production wipes cloud disks on PVC deletion. No backup path treats PV redundancy as backup — it is not. Running out of disk space or inodes stalls writes silently until apps crash. When pods stay in ContainerCreating, describe the pod and check FailedAttachVolume or FailedMount events.

Set allowVolumeExpansion: true on the StorageClass first. Patch the PVC request to the new size, for example from 20Gi to 40Gi, then verify inside the container with df -h on the mount path. Restart the pod if the filesystem does not auto-resize after the patch. Not every CSI driver supports online expansion — test in staging before production. Document expansion steps in your runbook alongside PV lifecycle management. Monitor used bytes and inodes continuously so you expand before applications fail writes, not after CrashLoopBackOff begins.

A standard disk around Rs 3,000/month (~USD 22) will bottleneck a busy MySQL instance — match tier to query load.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: