Skip to content

Volumes and Volume Types

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.

Terminal window
# The container restarts, but the Pod (and its volumes) does not disappear
kubectl get pod log-processor -o jsonpath='{.status.containerStatuses[*].restartCount}'
  • 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 / secret volumes — 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: v1
kind: Pod
metadata:
name: log-processor
spec:
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: true

Environment 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: v1
kind: Pod
metadata:
name: node-exporter
spec:
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: true

Declaring 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:

  1. spec.volumes[] — declares the volume and where its data comes from (emptyDir, hostPath, configMap, persistentVolumeClaim, and so on). This is Pod-scoped.
  2. Each container’s spec.containers[].volumeMounts[] — mounts that already-declared volume into that specific container’s filesystem, at a chosen mountPath.

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.

Terminal window
# Confirm both containers see the same file through the shared emptyDir
kubectl exec log-processor -c log-shipper -- ls /var/log/app

emptyDir 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:

k8s/volumes-and-volume-types/emptydir-memory.yaml
apiVersion: v1
kind: Pod
metadata:
name: emptydir-mem
spec:
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.yaml
pod/emptydir-mem created
$ kubectl exec emptydir-mem -- dd if=/dev/zero of=/cache/bigfile bs=1M count=20
dd: error writing '/cache/bigfile': No space left on device
11+0 records in
10+0 records out
10485760 bytes (10.0MB) copied, 0.004537 seconds, 2.2GB/s
command terminated with exit code 1
$ kubectl exec emptydir-mem -- df -h /cache
Filesystem Size Used Available Use% Mounted on
tmpfs 10.0M 10.0M 0 100% /cache
$ kubectl get pod emptydir-mem
NAME READY STATUS RESTARTS AGE
emptydir-mem 1/1 Running 0 61s

The 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 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:

k8s/volumes-and-volume-types/hostpath-worker.yaml
apiVersion: v1
kind: Pod
metadata:
name: hostpath-demo
spec:
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.yaml
pod/hostpath-demo created
$ kubectl exec hostpath-demo -- cat /data/marker.txt
from-worker-1
$ kubectl delete pod hostpath-demo --wait=true
pod "hostpath-demo" deleted
$ kubectl apply -f hostpath-worker2.yaml
pod/hostpath-demo created
$ kubectl exec hostpath-demo -- cat /data/marker.txt
cat: can't open '/data/marker.txt': No such file or directory
command terminated with exit code 1

On 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:

k8s/volumes-and-volume-types/projected.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: proj-cm
data:
app.properties: 'color=blue'
---
apiVersion: v1
kind: Pod
metadata:
name: projected-demo
spec:
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.yaml
configmap/proj-cm created
pod/projected-demo created
$ kubectl exec projected-demo -- ls -la /projected
lrwxrwxrwx 1 root root 21 Sep 26 12:17 app.properties -> ..data/app.properties
lrwxrwxrwx 1 root root 15 Sep 26 12:17 pod-name -> ..data/pod-name
lrwxrwxrwx 1 root root 15 Sep 26 12:17 sa-token -> ..data/sa-token
$ kubectl exec projected-demo -- cat /projected/app.properties
color=blue
$ kubectl exec projected-demo -- cat /projected/pod-name
projected-demo
$ kubectl exec projected-demo -- head -c 60 /projected/sa-token
eyJhbGciOiJSUzI1NiIsImtpZCI6IkdlTVVGWmlOWkpjMjF2WnNQN0ptcS1Y

All 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:

k8s/volumes-and-volume-types/generic-ephemeral.yaml
apiVersion: v1
kind: Pod
metadata:
name: ephemeral-vol-demo
spec:
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.yaml
pod/ephemeral-vol-demo created
$ kubectl get pod ephemeral-vol-demo
NAME READY STATUS RESTARTS AGE
ephemeral-vol-demo 1/1 Running 0 6s
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
ephemeral-vol-demo-scratch Bound pvc-cd12e877-e1da-4ec2-9535-7593a028ee79 32Mi RWO standard 6s

A 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.

When reviewing generated volume manifests, check for:

  • hostPath used 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 Pod
  • emptyDir with medium: Memory and no sizeLimit — unbounded tmpfs usage counts directly against node memory with no cap at all
  • A subPath mount 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 path inside the projected directory, assumed to “just merge” — this is a real conflict the API rejects, not a silent last-write-wins

🐛 Spot the bug

k8s/volumes-and-volume-types/shared-cache.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: cache-writer
spec:
replicas: 3
selector:
matchLabels:
app: cache-writer
template:
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: DirectoryOrCreate
Show 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
Two containers in one Pod sharing a single emptyDir volume

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.

ChoiceBenefitCost
emptyDir medium: Memorytmpfs speed, no disk I/OCounts against node memory; sizeLimit eviction, cited above
hostPathDirect node-local access (log agents, device paths)Not portable across nodes, proven above; real security risk
Projected volumeOne mount point for SA token + ConfigMap + downward APIAll sources share one directory tree — a path collision is a real admission-time error
Generic ephemeral volumePer-Pod PVC lifecycle with no separate manifestNot reusable the way a StatefulSet’s volumeClaimTemplates PVC can be retained
subPathMount a single key without exposing the rest of a ConfigMap/SecretBreaks live-update propagation entirely — permanent, not just slower
  • “emptyDir with medium: Memory has no real limits.” Per the docs cited above, sizeLimit is enforced and the Pod is evicted for exceeding it — RAM-backed doesn’t mean unbounded.
  • “hostPath data 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 subPath mount 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.
What happens to the data in an emptyDir volume when the Pod that owns it is deleted
Why is hostPath considered risky to use
Why does mounting a volume into a container require both spec.volumes and volumeMounts
Which volume type is designed to combine multiple sources, such as a ConfigMap and a Secret, into one mounted directory
marker.txt was written while this same Pod ran on worker-1 earlier, then the Pod was deleted and recreated pinned to worker-2 as shown above. What happens to marker.txt?
apiVersion: v1
kind: Pod
metadata:
  name: hostpath-demo
spec:
  nodeSelector:
    kubernetes.io/hostname: worker-2
  volumes:
    - name: data
      hostPath:
        path: /tmp/hostpath-demo
        type: DirectoryOrCreate
A container writes 20Mi into /cache, which is backed by this volume. Per the Kubernetes docs, what happens?
volumes:
  - name: cache
    emptyDir:
      medium: Memory
      sizeLimit: 10Mi