Persistent Storage with PVCs

Introduction: Beyond Temporary Storage

In previous lessons, we've been working exclusively with emptyDir volumes. These volumes are incredibly useful for temporary data, caching, and sharing information between containers in the same Pod. However, they have one critical limitation: they're tied to the Pod's lifecycle. When you delete a Pod, the emptyDir volume and all its data disappear with it. For many real-world applications — databases, user uploads, application state — this simply won't work.

You need storage that survives Pod deletions, restarts, and even migrations to different nodes. In this lesson, we'll introduce PersistentVolumeClaims (PVCs), which allow you to request durable storage that exists independently of any Pod. You'll learn how to create a storage request, connect it to a Pod, and verify that your data truly persists even when Pods come and go.

The Need for Decoupled Storage

Imagine you're running a blog application in Kubernetes. Users upload images, write posts, and build up content over time. If you store all that data in an emptyDir volume, what happens when you need to update your application? You'd create a new Pod with the updated code, delete the old Pod, and suddenly all your users' content is gone. That's obviously unacceptable for a production system. The fundamental problem is that emptyDir couples storage to the Pod lifecycle. The storage lives and dies with the Pod, which makes sense for temporary scratch space but not for valuable data.

What we need is a way to decouple storage from Pods. Instead of defining storage as part of the Pod specification, we want to request storage separately and then connect Pods to that storage. This separation means the storage can exist before any Pod uses it, and it continues to exist after Pods are deleted. You can delete a Pod, create a new one, and have it pick up exactly where the old one left off because the data is still there. This is the core idea behind PersistentVolumeClaims.

A PersistentVolumeClaim is essentially a request for storage. You're telling Kubernetes, "I need 5GB of storage that one Pod can write to at a time," and Kubernetes finds or creates storage that meets those requirements. The Pod doesn't need to know whether that storage is backed by a local disk, a network file system, or a cloud provider's block storage service. The Pod just says, "I want to use the storage from this claim," and Kubernetes handles the rest. This abstraction is powerful because it lets you write Pod specifications that work across different environments without worrying about the underlying storage infrastructure.

Defining a PersistentVolumeClaim

Let's look at how to actually create a PersistentVolumeClaim. Here's a complete example that requests 1GB of storage:

YAML
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

The structure should look familiar if you've worked with other Kubernetes resources. We have the standard apiVersion, kind, and metadata fields. The kind is PersistentVolumeClaim, which tells Kubernetes this is a storage request. The metadata.name field gives this claim a name — app-data-claim in this case — which we'll use later to reference it from a Pod.

The interesting part is the spec section, which describes what kind of storage we're requesting. Let's break down each field:

YAML
  accessModes:
    - ReadWriteOnce

The accessModes field specifies how the storage can be accessed. ReadWriteOnce means the volume can be mounted as read-write by a single node at a time. This is the most common access mode and works well for most applications. Other options include ReadWriteMany (multiple nodes can mount it for reading and writing simultaneously) and ReadOnlyMany (multiple nodes can mount it, but only for reading). For now, ReadWriteOnce is what you'll use in most scenarios — it allows Pods on a single node to read and write to the storage.

YAML
  resources:
    requests:
      storage: 1Gi

The resources.requests.storage field specifies how much storage you need. In this example, we're requesting 1 gibibyte (1Gi). You can request storage in various units: Mi for mebibytes, Gi for gibibytes, Ti for tebibytes. The amount you request should match your application's needs. If you're storing a small configuration database, 1Gi might be plenty. If you're storing video files, you might need 100Gi or more. Kubernetes will try to provision storage that meets or exceeds your request.

To create this PersistentVolumeClaim, save the YAML to a file called pvc.yaml and run:

Shell
kubectl apply -f pvc.yaml

You'll see output confirming the claim was created:

text
persistentvolumeclaim/app-data-claim created

At this point, you've made a request for storage, but you haven't used it yet. The claim exists independently, waiting for a Pod to connect to it.

Connecting Pods to Claims

Now that we have a PersistentVolumeClaim, let's create a Pod that uses it. Here's a Pod specification that mounts our claim:

YAML
apiVersion: v1
kind: Pod
metadata:
  name: app-using-pvc
spec:
  containers:
    - name: app
      image: nginx:1.25
      volumeMounts:
        - name: data-vol
          mountPath: /usr/share/nginx/html
  volumes:
    - name: data-vol
      persistentVolumeClaim:
        claimName: app-data-claim

The container and volumeMounts sections should look familiar from previous lessons. We're running an Nginx container and mounting a volume named data-vol at /usr/share/nginx/html, which is the default directory where Nginx serves web content from. The key difference is in the volumes section at the bottom:

YAML
  volumes:
    - name: data-vol
      persistentVolumeClaim:
        claimName: app-data-claim

Instead of defining an emptyDir volume, we're defining a persistentVolumeClaim volume. This type of volume references an existing PVC by name using the claimName field. We're telling Kubernetes, "For this volume named data-vol, use the storage from the PersistentVolumeClaim called app-data-claim." Notice that the Pod specification doesn't say anything about how much storage there is, what type of storage it is, or where it's located. The Pod simply references the claim, and Kubernetes handles connecting it to the actual storage.

This separation is elegant because it means you can write a Pod specification once and use it in different environments. In a development cluster, app-data-claim might be backed by local disk storage. In production, it might be backed by high-performance SSD storage from a cloud provider. The Pod doesn't need to change — only the underlying storage infrastructure changes.

Save this Pod specification to a file called pod-with-pvc.yaml and create it:

Shell
kubectl apply -f pod-with-pvc.yaml

You'll see:

text
pod/app-using-pvc created

The Pod is now running and has access to the persistent storage we requested. Any data written to /usr/share/nginx/html inside the container will be stored in the PVC, not in the container's ephemeral filesystem.

The Binding Process and Dynamic Provisioning

When you create a PersistentVolumeClaim, Kubernetes needs to find or create actual storage to fulfill your request. This process is called binding, and it involves multiple layers of abstraction working together. Here's how the components relate to each other:

PVC Binding Diagram

As the diagram shows, Pods reference PVCs by name, not the underlying storage directly. When you create a PVC, Kubernetes finds or creates a PersistentVolume (PV) that matches your request and binds the PVC to that PV. The PV then connects to the actual physical storage, whether that's a local disk, network filesystem, or cloud storage. This three-layer architecture is what enables the decoupling we've been discussing — Pods don't need to know about PVs or physical storage details, they only need to know the PVC name.

You can observe the binding process by checking the status of your PVC. Right after creating the claim, run:

Shell
kubectl get pvc app-data-claim

You'll likely see output like this:

text
NAME             STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
app-data-claim   Pending                                      standard       5s

The STATUS column shows Pending, which means the PVC is waiting for storage to be provisioned. In many production environments, especially with cloud providers, the Pending status will persist until you actually create a Pod that uses the PVC. This is due to a feature called Volume Binding Mode.

Volume Binding Modes determine when a PVC gets bound to actual storage:

  • Immediate: Storage is provisioned as soon as you create the PVC, even without a Pod
  • WaitForFirstConsumer: Storage provisioning waits until a Pod that uses the PVC is scheduled

The WaitForFirstConsumer mode is common in production because it ensures volumes are created in the same availability zone or region as the Pod that will use them. This is more efficient and avoids cross-zone data transfer costs. If your PVC stays Pending, don't worry — this is normal behavior with WaitForFirstConsumer. The storage will be provisioned once you create a Pod that references the PVC.

Dynamic Provisioning is the Kubernetes feature that automatically creates the underlying storage when you create a PVC. Instead of requiring a cluster administrator to manually set up storage volumes in advance, Kubernetes can talk to your cloud provider or storage system and say, "Create a 2GB volume for me right now." This happens behind the scenes, and you don't need to do anything special to enable it — it's configured at the cluster level through StorageClasses.

After you create your Pod (as shown earlier in the lesson), check the PVC status again:

Shell
kubectl get pvc app-data-claim

Now you should see:

text
NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
app-data-claim   Bound    pvc-a1b2c3d4-e5f6-7890-abcd-ef1234567890   1Gi        RWO            standard       2m

The STATUS is now Bound, which means Kubernetes has successfully provisioned storage and connected it to your claim. The VOLUME column shows the name of the underlying PersistentVolume that was created. The CAPACITY confirms you got the 1Gi you requested, and ACCESS MODES shows RWO (short for ReadWriteOnce). The Bound status is your confirmation that everything is wired up correctly and your Pod can now read and write to persistent storage.

Proving Persistence Beyond Pod Lifecycle

The whole point of using a PersistentVolumeClaim is that data should survive Pod deletions. Let's prove this works by writing some data, deleting the Pod, creating a new Pod with the same PVC, and verifying the data is still there. First, let's write a simple HTML file to the persistent volume:

Shell
kubectl exec -it app-using-pvc -- sh -c 'echo "<h1>Hello from PVC</h1>" > /usr/share/nginx/html/index.html'

This command runs inside the app-using-pvc Pod and writes an HTML file to /usr/share/nginx/html/index.html. Since /usr/share/nginx/html is mounted from our PVC, this data is being written to persistent storage. Let's verify the file exists:

Shell
kubectl exec -it app-using-pvc -- cat /usr/share/nginx/html/index.html

You should see:

text
<h1>Hello from PVC</h1>

Good — the data is there. Now comes the critical test. Let's completely delete the Pod:

Shell
kubectl delete pod app-using-pvc

You'll see:

text
pod "app-using-pvc" deleted

The Pod is gone. If we were using an emptyDir volume, the data would be gone too. But because we're using a PVC, the storage exists independently. Let's prove it by creating a new Pod that uses the same PVC. You can use the exact same pod-with-pvc.yaml file:

Shell
kubectl apply -f pod-with-pvc.yaml

A new Pod named app-using-pvc is created. Wait a few seconds for it to start, then check if our data is still there:

Shell
kubectl exec -it app-using-pvc -- cat /usr/share/nginx/html/index.html

You should see:

text
<h1>Hello from PVC</h1>

The data survived! This is the key difference between emptyDir and PersistentVolumeClaims. The Pod was completely deleted and recreated, but because the storage is decoupled from the Pod lifecycle, the data persisted. This is exactly what you need for production applications where data must survive deployments, updates, and failures.

It's worth noting that the PVC itself still exists even after you delete the Pod. You can verify this:

Shell
kubectl get pvc app-data-claim

You'll still see the claim with Bound status. The PVC and its underlying storage will continue to exist until you explicitly delete the PVC itself. This means you can delete and recreate Pods as many times as you want, and they'll all have access to the same persistent data as long as they reference the same PVC.

Summary and Practice Preview

PersistentVolumeClaims solve the fundamental problem of data persistence in Kubernetes by decoupling storage from Pod lifecycles. Instead of defining storage as part of a Pod, you create a separate storage request (the PVC) that specifies how much storage you need and how it should be accessed. Pods then reference that claim by name, and Kubernetes handles connecting them to the actual storage. The Bound status indicates that storage has been successfully provisioned, often through Dynamic Provisioning, which automatically creates the underlying storage resources.

Most importantly, data written to a PVC survives Pod deletions and recreations, making PVCs essential for any application that needs to maintain state. In the upcoming practice exercises, you'll create your own PVCs, connect them to Pods, and verify that data truly persists across Pod lifecycle events, giving you hands-on experience with this critical Kubernetes storage pattern.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal