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
- Kubernetes1.36.4
- Cluster4 nodes
- Runtimecontainerd 2.2.6
- CNICalico v3.32.1
- TimeAbout 16 min
- Reviewed22 August 2026
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.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA1001 | 192.168.0.175 | Ubuntu 26.04 LTS | Control Plane Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE01 | 192.168.0.176 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE02 | 192.168.0.177 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE03 | 192.168.0.178 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- A running cluster with more than one worker node. The hostPath step needs two nodes to make its point.
- A namespace to work in. This session uses
stor, and Pods are pinned to named nodes withnodeSelectorso the node-local behaviour is reproducible rather than luck.
-
emptyDir: shared inside the Pod
One Pod, two containers, one
emptyDirvolume mounted into both - at/datain the writer and/sharedin the reader. The writer echoes a line at startup and the reader can read it:written-by-writerAn 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: Memorymakes it a tmpfs, so it is RAM rather than disk, and thesizeLimitis real.dftells the two apart -/datais on the node's filesystem,/cacheis 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-2zlfcExpected 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.
-
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 1Expected 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.
-
hostPath: survives the Pod, belongs to one node
A
hostPathvolume mounts a directory from the node's own filesystem. A Pod pinned tonode01writes a marker and can read it back:from-node01-podThat file is on node01's disk, so it outlives the Pod. Now the catch. A second Pod, same YAML, same
/var/tmp/cg-hostpathpath, pinned tonode02- 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-node02Expected resultThe marker exists on node01 and does not exist on node02, from the same path.
Success conditionSame path, two nodes, two different directories.
-
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 staysPending, and the event names a reason worth reading carefully:Warning ProvisioningFailed storageclass.storage.k8s.io "manual" not foundTwo separate things failed. No existing volume satisfies 5Gi (the only one is 1Gi), and there is no StorageClass object called
manualto 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-pvBinding 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-pvExpected 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
A file written by a Pod is gone after the Pod restarts.
Why: It was in an emptyDir, or in the container's own writable layer. Neither survives the Pod.
Fix:Check what the mount actually is:
kubectl get pod <pod> -o jsonpath='{.spec.volumes}'. Anything that must persist needs a PersistentVolumeClaim.A PVC sits in Pending and nothing happens.
Why: No existing PV satisfies it and no StorageClass can provision one - or the class named in the claim does not exist.
Fix:Read the events with
kubectl describe pvc <name>; they name the reason. Then comparekubectl get pvfor size and access mode against the claim, andkubectl get storageclassfor the class it asks for.A claim reports more capacity than it requested.
Why: That is correct. Binding is whole-volume - a claim gets the PV it bound to, and its capacity.
Fix:Nothing to fix. If the difference matters, use dynamic provisioning so the volume is created at the requested size instead of being matched to a pre-made one.
A workload using hostPath behaves differently after being rescheduled.
Why: hostPath is the node's filesystem. On a different node the same path is a different directory, usually empty.
Fix:Use a PersistentVolumeClaim for anything that has to follow the workload. Reserve hostPath for agents that genuinely want the node they are running on.