Volumes in Pods — emptyDir, hostPath, projected, and CSI
What you'll learn
- Configure volumes in spec.volumes and mount them in containers
- Distinguish emptyDir, hostPath, projected, and CSI volumes
- Reason about volume lifecycle relative to Pod lifecycle
- Avoid the common misconfigurations (hostPath, subPath)
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
Volumes in a Pod spec tie filesystem state to a Pod’s
lifecycle. Some volume types are ephemeral (emptyDir); some
are durable (CSI/PVC); some are projections of cluster objects
(ConfigMap, Secret). This lesson covers the volume types that
matter in production, the lifecycle semantics of each, and
the discipline around hostPath and subPath.
How volumes work
A volume in a Pod has three parts:
- Spec —
spec.volumesdeclares the volume’s name and type. The type determines where the data lives (empty directory on the node, a directory on the host, a CSI- provisioned network filesystem, etc.). - Mount —
spec.containers[*].volumeMounts(orinitContainers[*].volumeMounts) declares which containers mount the volume and at what path. - Lifecycle — the kubelet creates the volume when the Pod starts and removes it when the Pod terminates (for ephemeral types) or releases it back to the storage system (for durable types).
flowchart LR
Pod --> Vol["spec.volumes: data (emptyDir)"]
Vol --> MountA["Container A: mount /var/data"]
Vol --> MountB["Container B: mount /var/cache"]
Pod --> Kubelet["kubelet creates volume on node"]
Kubelet -->|"Pod terminates"| Cleanup["Volume deleted (emptyDir)"]
emptyDir — ephemeral scratch
volumes:
- name: scratch
emptyDir: {}
An emptyDir is a directory on the node’s local filesystem
that exists for the lifetime of the Pod. When the Pod is
created, the directory is empty. When the Pod terminates, the
directory is deleted.
Three sub-options:
sizeLimit: a maximum size; if exceeded, the Pod is evicted. Useful to prevent runaway log fills.medium: Memory: a tmpfs (RAM-backed) directory. Fast; lost on node reboot.medium: ""(default): node-local disk. Backed by the node’s filesystem; lost on node reboot.
Use cases:
- Scratch space for a workload that needs writable disk (a cache, a build directory).
- Sidecar communication: one container writes a file, another reads it (e.g., a log shipper reading a log file produced by the main container).
- Init container output: an init container computes a
value, writes it to a shared
emptyDir, the main container reads it.
flowchart LR
Init[Init container] -->|"writes /tmp/result"| Empty["emptyDir /shared"]
Empty --> Main[Main container]
Empty --> Side[Sidecar]
hostPath — escape to the node
volumes:
- name: host-etc
hostPath:
path: /etc/something
type: Directory
hostPath mounts a directory or file from the node’s
filesystem into the Pod. The Pod sees the same data as the
host processes.
Use cases (legitimate):
- Node agents: log shippers reading
/var/log, node exporters reading/proc, CNI plugins reading/etc/cni/net.d. - DaemonSets that need host access: monitoring agents, storage agents.
Use cases (illegitimate):
- Application data persistence: use a PVC instead.
- Sharing configuration across Pods: use a ConfigMap.
- Anything that breaks Pod portability: a Pod with a hostPath only works on nodes with the expected path.
volumes:
- name: data
hostPath:
path: /mnt/data/web
type: DirectoryOrCreate
The above is a common anti-pattern. The Pod will only work on
nodes that have /mnt/data/web; on other nodes, the kubelet
creates it (with DirectoryOrCreate) and the data is local
to the node. If the Pod is rescheduled, it loses its data.
projected — composite volumes
volumes:
- name: config
projected:
sources:
- configMap:
name: app-config
- secret:
name: app-secret
- downwardAPI:
items:
- path: pod-name
fieldRef:
fieldPath: metadata.name
- path: pod-ip
fieldRef:
fieldPath: status.podIP
- serviceAccountToken:
path: token
audience: api
expirationSeconds: 3600
A projected volume is a composite of multiple sources, mounted as a single directory. Each source contributes one or more files. Common sources:
- configMap: each key becomes a file named
<key>in the directory. - secret: each key becomes a file (with the value as the contents).
- downwardAPI: expose Pod metadata as files (Pod name, namespace, IP, labels, annotations).
- serviceAccountToken: project a token bound to the Pod’s ServiceAccount, with a configurable audience and lifetime.
Use cases:
- Configuration bundles: combine ConfigMap + Secret + DownwardAPI into a single volume the application reads.
- Service-account tokens: project short-lived tokens for the application to authenticate to the API server (e.g., a sidecar that calls the Kubernetes API).
- Hot-reload configuration: mount ConfigMap as a
projected volume; the kubelet updates the projected files
when the ConfigMap changes (within the
configMapAndSecretReloadDetectionStrategywindow).
CSI / PVC — durable storage
volumes:
- name: data
persistentVolumeClaim:
claimName: web-data
A persistentVolumeClaim references a PersistentVolumeClaim
in the same namespace. The PVC was bound to a PersistentVolume
by the storage controller (or a dynamic provisioner via the
StorageClass). The PV was provisioned by a CSI driver.
Production discipline: CSI / PVC is the right tool for any data that must survive a Pod reschedule.
flowchart LR
Pod -->|claimName| PVC["PersistentVolumeClaim"]
PVC -->|bound to| PV["PersistentVolume"]
PV -->|provisioned by| SC["StorageClass"]
SC -->|driver| CSI[CSI plugin]
CSI -->|provisioning| Backend["Network storage<br/>(EBS, Ceph, NFS, ...)"]
The full flow (Part XLIX-Kubernetes-Volumes covers in depth):
- Pod spec references a PVC.
- PVC references a StorageClass and asks for a size.
- The storage provisioner creates a PV backed by the CSI driver’s storage.
- The PV is bound to the PVC.
- The kubelet on the chosen node mounts the PV into the Pod.
- The Pod reads/writes to the mount path; data persists in the backend storage.
subPath — escape a single file
volumes:
- name: config
configMap:
name: app-config
containers:
- name: app
volumeMounts:
- name: config
mountPath: /etc/app/app.conf
subPath: app.conf
subPath mounts a single file or subdirectory from a volume
into a container, rather than the entire volume. Useful when:
- The volume is a ConfigMap with multiple keys and you want only one.
- The volume is a PVC and you want a subdirectory.
volumes:
- name: data
emptyDir: {}
containers:
- name: app
volumeMounts:
- name: data
mountPath: /cache
subPath: cache-v1
This creates /cache as a subdirectory of the emptyDir
without exposing the emptyDir’s other contents.
The volume lifecycle
| Type | Lifecycle | When data is deleted |
|---|---|---|
| emptyDir | Pod | When Pod terminates |
| hostPath | Node | When manually deleted (Pod termination does not delete) |
| projected | Pod (ConfigMap/Secret sources are cluster-wide) | When Pod terminates (the projection is recreated each Pod start) |
| PVC/CSI | Independent of Pod | When PVC is deleted and reclaimPolicy applies |
The key distinction:
- Pod-scoped (emptyDir, projected): data dies with the Pod. Good for scratch, sidecar communication, ephemeral config.
- Node-scoped (hostPath): data persists on the node. Used for node agents; broken for application portability.
- Cluster-scoped (PVC/CSI): data is independent of Pod and node. The right tool for durable storage.
Cross-course references
- The Linux course part
XVII-Linux-RAIDandXVIII-Linux-Storagecover block storage; CSI volumes are the cluster-level equivalent. - The Docker course part
XXXII-Docker-Storagecovers Docker volumes; Kubernetes PVCs are the cluster-level equivalent. - The Proxmox course part
XXXV-Linux-Storagecovers shared storage; CSI plugins like Ceph RBD and NFS are the cluster-level equivalent.
Quiz
Knowledge check · 4 questions
Q1. Which volume type is the worst choice for application Pods that need persistent storage across reschedules?
Q2. A ConfigMap mounted with `subPath: app.conf` will pick up ConfigMap updates without a Pod restart.
Q3. A workload writes to an `emptyDir` for a local cache. The cache grows unbounded over time and fills the node's root filesystem. Diagnose what happens and how to prevent it.
Application Pod `web-7c8` mounts `emptyDir: {}` at `/cache`. The application writes to `/cache/index` as a local cache. The cache grows over time. After a week, the Pod's emptyDir has consumed 50 GB on the node's root partition.
Q4. When is `projected` volume the right choice instead of mounting a ConfigMap directly?
Passing score: 75%. Answers are checked in this browser.
Production discipline
- Set
sizeLimiton everyemptyDirin production. Unbounded emptyDirs are a node-stability risk. - Avoid
hostPathfor application Pods. Reserve it for node agents. PSSrestrictedrejects most hostPath types. - Use PVC for data that must survive reschedules. CSI- provisioned storage is the standard.
- Mount ConfigMaps as volumes (not subPath). subPath breaks kubelet’s refresh; the container sees stale data until restart.
- Use projected volumes for config bundles. One mount point with multiple sources; the application reads from a single directory.