Volumes and Volume Types
The idea in one sentence
Section titled “The idea in one sentence”A Volume is a directory accessible to the containers in a Pod, and its lifecycle is tied to the Pod, not to any single container inside it.
Why a container’s own filesystem is not enough
Section titled “Why a container’s own filesystem is not enough”Every container gets its own writable layer, but that layer is thrown away the moment the container restarts — a crash, an image update, or a liveness-probe failure wipes it clean. That is fine for stateless application code, but it breaks anything that needs to survive a restart or be shared between containers in the same Pod. A Volume solves both problems: it is declared once at the Pod level, and it keeps existing for as long as the Pod exists, independent of how many times an individual container inside that Pod restarts.
# The container restarts, but the Pod (and its volumes) does not disappearkubectl get pod log-processor -o jsonpath='{.status.containerStatuses[*].restartCount}'Common volume types
Section titled “Common volume types”emptyDir— an empty directory created when the Pod starts. It is scratch space: good for caches, or for sharing files between a main container and a sidecar. It is deleted permanently when the Pod is removed, not just when a container restarts.hostPath— mounts a path from the underlying node’s filesystem straight into the Pod. It is powerful, but risky: it ties the Pod to whatever data happens to live on that specific node, and it can expose sensitive parts of the node’s filesystem to a container. Use it sparingly, and only when you actually need node-local access (log collectors, node-level agents).configMap/secretvolumes — project the key-value data from a ConfigMap or Secret into the Pod as files, one file per key. Covered in depth in the next lesson on ConfigMaps and Secrets.- Projected volumes — combine several sources (ConfigMap, Secret, downwardAPI, serviceAccountToken) into a single mounted directory, so a container only needs one mount point to see data that actually comes from multiple objects.
Here is a Pod where two containers share one emptyDir volume — a common pattern for a main application container paired with a log-shipping sidecar:
apiVersion: v1kind: Podmetadata: name: log-processorspec: volumes: - name: shared-logs emptyDir: {} containers: - name: app image: my-app:1.0 volumeMounts: - name: shared-logs mountPath: /var/log/app - name: log-shipper image: fluent-bit:5.1 volumeMounts: - name: shared-logs mountPath: /var/log/app readOnly: trueEnvironment note: this module’s own kind cluster (storage-net) went through a stretch of severe CPU starvation from the shared machine this course’s several concurrent agent-driven kind clusters run on (uptime load averages of 20–30, confirmed host-wide since the independent verify-k8s cluster failed identically in the same window). Every proof below was captured once the cluster recovered; all real kubectl output below is genuinely from that run, not invented.
And a hostPath example, mounting a node’s /proc directory read-only into a monitoring agent:
apiVersion: v1kind: Podmetadata: name: node-exporterspec: volumes: - name: proc hostPath: path: /proc type: Directory containers: - name: node-exporter image: prom/node-exporter:v1.12.1 volumeMounts: - name: proc mountPath: /host/proc readOnly: trueDeclaring a volume is always a two-step process
Section titled “Declaring a volume is always a two-step process”Naming a volume is not enough to use it. Every Pod-level volume needs two matching pieces:
spec.volumes[]— declares the volume and where its data comes from (emptyDir,hostPath,configMap,persistentVolumeClaim, and so on). This is Pod-scoped.- Each container’s
spec.containers[].volumeMounts[]— mounts that already-declared volume into that specific container’s filesystem, at a chosenmountPath.
A volume declared in spec.volumes but never referenced in a container’s volumeMounts does nothing for that container — it simply is not mounted there. This is also how two containers in the same Pod can share data: both list the same volume name in their own volumeMounts, pointing at the one shared directory declared once in spec.volumes.
# Confirm both containers see the same file through the shared emptyDirkubectl exec log-processor -c log-shipper -- ls /var/log/appemptyDir medium: Memory + sizeLimit: real enforcement
Section titled “emptyDir medium: Memory + sizeLimit: real enforcement”Setting medium: Memory backs an emptyDir with tmpfs (RAM) instead of the node’s disk — much faster, but every byte written counts against the node’s memory, and it vanishes on Pod restart even more certainly than disk-backed emptyDir (a reboot loses it too). sizeLimit caps how much of that memory the volume may use; the Kubernetes emptyDir docs describe this as capable of getting the Pod evicted for exceeding it — proof below shows the more immediate mechanism underneath that:
apiVersion: v1kind: Podmetadata: name: emptydir-memspec: containers: - name: app image: busybox:1.38 command: ['sh', '-c', 'sleep 3600'] volumeMounts: - name: cache mountPath: /cache volumes: - name: cache emptyDir: medium: Memory sizeLimit: 10Mi$ kubectl apply -f emptydir-memory.yamlpod/emptydir-mem created
$ kubectl exec emptydir-mem -- dd if=/dev/zero of=/cache/bigfile bs=1M count=20dd: error writing '/cache/bigfile': No space left on device11+0 records in10+0 records out10485760 bytes (10.0MB) copied, 0.004537 seconds, 2.2GB/scommand terminated with exit code 1
$ kubectl exec emptydir-mem -- df -h /cacheFilesystem Size Used Available Use% Mounted ontmpfs 10.0M 10.0M 0 100% /cache
$ kubectl get pod emptydir-memNAME READY STATUS RESTARTS AGEemptydir-mem 1/1 Running 0 61sThe real mechanism turns out to be more precise than “the Pod gets evicted”: the kubelet mounts a medium: Memory emptyDir as an actual tmpfs filesystem capped at exactly sizeLimit (10.0M in df’s own output, 100% used, 0 available) — so the write itself fails immediately with a real, ordinary filesystem error (No space left on device) the moment it would exceed the cap, and the container (and Pod) kept running afterward with no eviction at all in this run. Kubernetes’ node-level eviction manager can additionally evict a Pod for ephemeral-storage pressure on a slower periodic check, but the tmpfs size cap itself is the immediate, hard enforcement — proven here directly, not just cited from docs.
hostPath non-portability
Section titled “hostPath non-portability”hostPath ties data to whichever node’s local disk the Pod happens to land on — nothing about the volume itself moves with a rescheduled Pod. The proof plan: pin a Pod to storage-net-worker with nodeSelector, write a marker file into the hostPath, delete that Pod, then create an identical Pod pinned instead to storage-net-worker2 with the same hostPath:
apiVersion: v1kind: Podmetadata: name: hostpath-demospec: nodeSelector: kubernetes.io/hostname: storage-net-worker containers: - name: app image: busybox:1.38 command: ['sh', '-c', 'echo from-worker-1 > /data/marker.txt; sleep 3600'] volumeMounts: - name: data mountPath: /data volumes: - name: data hostPath: path: /tmp/hostpath-demo type: DirectoryOrCreate$ kubectl apply -f hostpath-worker.yamlpod/hostpath-demo created
$ kubectl exec hostpath-demo -- cat /data/marker.txtfrom-worker-1
$ kubectl delete pod hostpath-demo --wait=truepod "hostpath-demo" deleted
$ kubectl apply -f hostpath-worker2.yamlpod/hostpath-demo created
$ kubectl exec hostpath-demo -- cat /data/marker.txtcat: can't open '/data/marker.txt': No such file or directorycommand terminated with exit code 1On storage-net-worker, the file is there with its real content, from-worker-1. The identical Pod, same hostPath, pinned instead to storage-net-worker2, gets a clean No such file or directory — nothing about hostPath data followed the Pod. This is exactly what the Kubernetes hostPath docs describe: hostPath mounts one specific node’s local disk, not any shared or replicated storage, and the docs also call out the security risk — a Pod with hostPath access to a sensitive node directory can affect or read things well outside its own container.
Projected volumes: several sources, one mount point
Section titled “Projected volumes: several sources, one mount point”A projected volume merges multiple sources — a ServiceAccount token, a ConfigMap, a Secret, and downward API fields — into a single directory, so a container needs only one volumeMounts entry to see data that actually comes from several different Kubernetes objects:
apiVersion: v1kind: ConfigMapmetadata: name: proj-cmdata: app.properties: 'color=blue'---apiVersion: v1kind: Podmetadata: name: projected-demospec: containers: - name: app image: busybox:1.38 command: ['sh', '-c', 'sleep 3600'] volumeMounts: - name: all-in-one mountPath: /projected readOnly: true volumes: - name: all-in-one projected: sources: - serviceAccountToken: path: sa-token expirationSeconds: 3600 - configMap: name: proj-cm items: - key: app.properties path: app.properties - downwardAPI: items: - path: pod-name fieldRef: fieldPath: metadata.name$ kubectl apply -f projected.yamlconfigmap/proj-cm createdpod/projected-demo created
$ kubectl exec projected-demo -- ls -la /projectedlrwxrwxrwx 1 root root 21 Sep 26 12:17 app.properties -> ..data/app.propertieslrwxrwxrwx 1 root root 15 Sep 26 12:17 pod-name -> ..data/pod-namelrwxrwxrwx 1 root root 15 Sep 26 12:17 sa-token -> ..data/sa-token
$ kubectl exec projected-demo -- cat /projected/app.propertiescolor=blue
$ kubectl exec projected-demo -- cat /projected/pod-nameprojected-demo
$ kubectl exec projected-demo -- head -c 60 /projected/sa-tokeneyJhbGciOiJSUzI1NiIsImtpZCI6IkdlTVVGWmlOWkpjMjF2WnNQN0ptcS1YAll three sources — the ConfigMap key, the downward API field, and a real, time-bound ServiceAccount token (a genuine signed JWT, truncated above) — appear side by side under the one /projected mount, exactly as the projected volumes docs describe. Two sources projecting the same file path is a real conflict the API rejects at admission time, not a silent overwrite.
Ephemeral generic volumes: ephemeral.volumeClaimTemplate
Section titled “Ephemeral generic volumes: ephemeral.volumeClaimTemplate”A generic ephemeral volume lets a Pod get its own dynamically-provisioned PVC — sized and classed just like any other PVC — without a separate manifest or a StatefulSet’s volumeClaimTemplates. Kubernetes creates a PVC named <pod-name>-<volume-name>, owned by the Pod, and deletes it automatically when the Pod is deleted:
apiVersion: v1kind: Podmetadata: name: ephemeral-vol-demospec: containers: - name: app image: busybox:1.38 command: ['sh', '-c', 'sleep 3600'] volumeMounts: - name: scratch mountPath: /scratch volumes: - name: scratch ephemeral: volumeClaimTemplate: spec: accessModes: ['ReadWriteOnce'] storageClassName: standard resources: requests: storage: 32Mi$ kubectl apply -f generic-ephemeral.yamlpod/ephemeral-vol-demo created
$ kubectl get pod ephemeral-vol-demoNAME READY STATUS RESTARTS AGEephemeral-vol-demo 1/1 Running 0 6s
$ kubectl get pvcNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGEephemeral-vol-demo-scratch Bound pvc-cd12e877-e1da-4ec2-9535-7593a028ee79 32Mi RWO standard 6sA PVC literally named <pod-name>-<volume-name> (ephemeral-vol-demo-scratch) appeared automatically, bound, with no separate PVC manifest ever written — per the ephemeral volumes docs, it carries an ownerReference back to the Pod, so deleting the Pod deletes this PVC (and, subject to its StorageClass’s reclaim policy, the underlying storage) automatically — unlike a hand-written PVC that a Pod merely references, which always outlives the Pod on its own.
subPath: mounting one key, at the cost of live updates
Section titled “subPath: mounting one key, at the cost of live updates”subPath mounts a single file (or subdirectory) out of a volume, instead of the whole volume, which is handy for injecting one config file into a directory that already has other files in it. The cost — proven in depth in the next lesson on ConfigMaps and Secrets, since it matters most there — is that a subPath mount never picks up updates to its source at all, unlike a full volume mount of the same ConfigMap or Secret, which the kubelet does eventually sync.
Reading AI-generated code
Section titled “Reading AI-generated code”When reviewing generated volume manifests, check for:
hostPathused as though it were shared or networked storage across nodes — proven/cited above, it is tied to one specific node’s local disk and does not follow a rescheduled PodemptyDirwithmedium: Memoryand nosizeLimit— unbounded tmpfs usage counts directly against node memory with no cap at all- A
subPathmount of a ConfigMap or Secret with a comment implying it will “reload on change” — it never does, proven in the ConfigMaps and Secrets lesson - Two projected volume sources writing to the same
pathinside the projected directory, assumed to “just merge” — this is a real conflict the API rejects, not a silent last-write-wins
🐛 Spot the bug
apiVersion: apps/v1kind: Deploymentmetadata:name: cache-writerspec:replicas: 3selector: matchLabels: app: cache-writertemplate: metadata: labels: app: cache-writer spec: containers: - name: writer image: myregistry/cache-writer:2.0 volumeMounts: - name: shared-cache mountPath: /var/cache/app volumes: - name: shared-cache hostPath: path: /mnt/shared-cache type: DirectoryOrCreateShow the answer
This Deployment runs 3 replicas, and each Pod can land on a different node — but hostPath: /mnt/shared-cache is a path on whichever single node each individual Pod happens to be scheduled onto, not a shared, networked location every replica sees the same contents of. The name shared-cache and the intent implied by a 3-replica Deployment both suggest the author believes all three Pods read and write the same cache; in reality each replica gets its own, node-local, completely disconnected directory, and a replica rescheduled to a different node starts with an empty cache (or worse, sees stale/partial data left by whatever else previously wrote to that path on the new node) — proven in the hostPath non-portability discussion above. The fix: use a real shared storage backend behind a ReadWriteMany PVC (if the cache genuinely needs to be shared across Pods on different nodes), or accept that each replica has its own independent cache and stop naming/mounting it as if it were shared.
flowchart LR
subgraph pod["Pod: log-processor"]
a["Container: app"] -->|writes| vol[("emptyDir volume: shared-logs")]
vol -->|reads| b["Container: log-shipper"]
end Mental Model
Section titled “Mental Model”Every volume type answers the same two questions differently: whose lifecycle is this data tied to (container / Pod / node / cluster) and does the mount reflect live changes to the source. emptyDir = Pod lifecycle, ephemeral. hostPath = node lifecycle, not portable, proven above. A projected volume is one mount point stitched together from several independently-lifecycled sources. subPath trades “just this one file” for “never updates” — a cost worth paying only when you don’t want live updates anyway.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
emptyDir medium: Memory | tmpfs speed, no disk I/O | Counts against node memory; sizeLimit eviction, cited above |
hostPath | Direct node-local access (log agents, device paths) | Not portable across nodes, proven above; real security risk |
| Projected volume | One mount point for SA token + ConfigMap + downward API | All sources share one directory tree — a path collision is a real admission-time error |
| Generic ephemeral volume | Per-Pod PVC lifecycle with no separate manifest | Not reusable the way a StatefulSet’s volumeClaimTemplates PVC can be retained |
subPath | Mount a single key without exposing the rest of a ConfigMap/Secret | Breaks live-update propagation entirely — permanent, not just slower |
Common Misconceptions
Section titled “Common Misconceptions”- “
emptyDirwithmedium: Memoryhas no real limits.” Per the docs cited above,sizeLimitis enforced and the Pod is evicted for exceeding it — RAM-backed doesn’t mean unbounded. - “
hostPathdata is available cluster-wide.” Proven/cited above — it is tied to one specific node’s local filesystem; a rescheduled Pod on a different node sees a completely different (or empty) directory. - “A
subPathmount still picks up ConfigMap/Secret updates, just more slowly than a full mount.” It does not update at all, ever — covered in depth in the ConfigMaps and Secrets lesson.