CertGrid CertGrid
Hands-on Lab·Kubernetes and Cloud Native Associate

Volume Types and What Each One Loses

An emptyDir shared by two containers, deleted with its Pod. A hostPath that survives the Pod but means different data on every node. Then a claim that binds to a whole PersistentVolume and gets 1Gi after asking for 500Mi - and a claim that stays Pending, for two reasons at once.

Storage Guide 22 of 46 Beginner

Written against the versions above. emptyDir and hostPath are core Kubernetes and behave the same everywhere. The PersistentVolume here is created by hand so the binding is visible on its own; in a real cluster a StorageClass usually creates the volume for you on demand, so you write only the claim.

Three workers, which is what makes the hostPath step land: the same path on two nodes is two different directories.
Server NameIP AddressOSRolesCPURAMHDD
CKA1001192.168.0.175Ubuntu 26.04 LTSControl Plane Node2 Core4 GB50 GB
CKA1001-NODE01192.168.0.176Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA1001-NODE02192.168.0.177Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA1001-NODE03192.168.0.178Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB

Before you start

  1. emptyDir: shared inside the Pod

    One Pod, two containers, one emptyDir volume mounted into both - at /data in the writer and /shared in the reader. The writer echoes a line at startup and the reader can read it:

    written-by-writer

    An emptyDir is scratch space shared by the containers of one Pod. Different mount paths, same directory. That is the normal way two containers in a Pod hand files to each other.

    The second volume shows the one option worth knowing: medium: Memory makes it a tmpfs, so it is RAM rather than disk, and the sizeLimit is real. df tells the two apart - /data is on the node's filesystem, /cache is a 64M tmpfs.

    bash Example session
    kubectl exec shared -n stor -c reader -- cat /shared/note.txtwritten-by-writerkubectl exec shared -n stor -c writer -- df -h /data /cache | grep -vE '^Filesystem'/dev/mapper/ubuntu--vg-ubuntu--lv                         47.1G     16.6G     28.3G  37% /datatmpfs                    64.0M         0     64.0M   0% /cachekubectl get pod shared -n stor -o jsonpath="{range .spec.volumes[*]}{.name}{\" \"}{.emptyDir}{\"\n\"}{end}"scratch {}memcache {"medium":"Memory","sizeLimit":"64Mi"}kube-api-access-2zlfc 

    Expected resultThe reader sees the writer's file, and df shows one volume on disk and one in tmpfs.

    Success conditionTwo containers, two mount paths, one directory.

  2. And it goes when the Pod goes

    Delete the Pod, create the same Pod again, look in /data:

    total 8
    drwxrwxrwx    2 root     root          4096 Aug 21 09:06 .
    drwxr-xr-x    1 root     root          4096 Aug 21 09:06 ..

    Empty. The file is gone and reading it exits 1.

    An emptyDir lives exactly as long as the Pod does. Not the container - a container that crashes and restarts finds its emptyDir intact - but the Pod. Delete the Pod, or have it rescheduled to another node, and the directory is created empty again. This is the single most common surprise in Kubernetes storage, and it is why anything you actually need to keep does not go in an emptyDir.

    bash Example session
    kubectl get pod shared -n stor -o jsonpath="{.spec.nodeName}{\"\n\"}"cka1001-node03kubectl delete pod shared -n storpod "shared" deleted from stor namespacekubectl exec shared -n stor -- ls -la /datatotal 8drwxrwxrwx    2 root     root          4096 Aug 21 09:06 .drwxr-xr-x    1 root     root          4096 Aug 21 09:06 ..kubectl exec shared -n stor -- cat /data/note.txt 2>&1 | tail -1command terminated with exit code 1

    Expected resultA recreated Pod finds /data empty, and reading the old file exits 1.

    Success conditionYou can say what an emptyDir's lifetime is tied to, precisely.

  3. hostPath: survives the Pod, belongs to one node

    A hostPath volume mounts a directory from the node's own filesystem. A Pod pinned to node01 writes a marker and can read it back:

    from-node01-pod

    That file is on node01's disk, so it outlives the Pod. Now the catch. A second Pod, same YAML, same /var/tmp/cg-hostpath path, pinned to node02 - and the directory is empty.

    hostPath is not cluster storage. It is one machine's disk. The path is the same on both nodes and the contents are unrelated, because they are different disks. A workload that gets rescheduled finds a different directory, or an empty one, with no error to tell it anything happened. That is why hostPath is for node-level agents that genuinely want the node's filesystem, not for application data.

    bash Example session
    kubectl exec hp-node01 -n stor -- cat /host/marker.txtfrom-node01-podkubectl exec hp-node02 -n stor -- ls -la /hosttotal 8drwxr-xr-x    2 root     root          4096 Aug 21 09:07 .drwxr-xr-x    1 root     root          4096 Aug 21 09:07 ..kubectl exec hp-node02 -n stor -- cat /host/marker.txt 2>&1 | tail -1command terminated with exit code 1kubectl get pods -n stor -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName --no-headershp-node01   cka1001-node01hp-node02   cka1001-node02

    Expected resultThe marker exists on node01 and does not exist on node02, from the same path.

    Success conditionSame path, two nodes, two different directories.

  4. A claim binds to a volume, not to a size

    Here is the pair KCNA actually asks about. A PersistentVolume is a piece of storage that exists in the cluster; a PersistentVolumeClaim is a request for some. A Pod names the claim, never the volume.

    One 1Gi PV, Available. Then a claim for 5Gi - which stays Pending, and the event names a reason worth reading carefully:

    Warning  ProvisioningFailed  storageclass.storage.k8s.io "manual" not found

    Two separate things failed. No existing volume satisfies 5Gi (the only one is 1Gi), and there is no StorageClass object called manual to create one on demand - so neither static binding nor dynamic provisioning can help. A Pending claim is the cluster saying "nothing here fits and I cannot make one", not an error.

    Then a claim for 500Mi, which does bind - and look at what it got:

    1Gi bound to manual-pv

    Binding is whole-volume. You asked for 500Mi, you were given the 1Gi volume, and the claim now reports 1Gi as its capacity. A PV binds to exactly one PVC and vice versa; the request is a minimum, not an allocation. The PV records who holds it, so the relationship is readable from both ends.

    bash Example session
    kubectl get pv manual-pvNAME        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGEmanual-pv   1Gi        RWO            Retain           Available           manual         <unset>                          0ssleep 8; kubectl get pvc too-big -n storNAME      STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGEtoo-big   Pending                                      manual         <unset>                 8skubectl describe pvc too-big -n stor | tail -6VolumeMode:    FilesystemUsed By:       <none>Events:  Type     Reason              Age   From                         Message  ----     ------              ----  ----                         -------  Warning  ProvisioningFailed  8s    persistentvolume-controller  storageclass.storage.k8s.io "manual" not foundsleep 8; kubectl get pvc -n storNAME      STATUS    VOLUME      CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGEfits      Bound     manual-pv   1Gi        RWO            manual         <unset>                 8stoo-big   Pending                                         manual         <unset>                 16skubectl get pv manual-pv -o jsonpath="{.status.phase}{\" claimed by \"}{.spec.claimRef.namespace}/{.spec.claimRef.name}{\"\n\"}"Bound claimed by stor/fitskubectl get pvc fits -n stor -o jsonpath="{.status.capacity.storage}{\" bound to \"}{.spec.volumeName}{\"\n\"}"1Gi bound to manual-pv

    Expected resultThe 5Gi claim stays Pending, the 500Mi claim binds, and the bound claim reports 1Gi.

    Success conditionYou asked for 500Mi and the claim reports 1Gi - and you can say why.

Troubleshooting

Official sources