Containers have ephemeral filesystems — when a container restarts, everything written inside is lost. Volumes solve this by providing storage that outlives individual containers. This lesson covers the primitive volume types that are built directly into the Pod spec — no external provisioning needed.
1. Volume Concepts
A volume has two parts in a Pod spec:
spec:
volumes: # 1. DEFINE the volume (what type, what source)
- name: my-volume
emptyDir: {}
containers:
- name: app
volumeMounts: # 2. MOUNT it into a container (where)
- name: my-volume
mountPath: /data
Lifetime Rules
| Volume Type | Lifetime | Survives Container Restart? | Survives Pod Deletion? |
|---|---|---|---|
emptyDir | Same as Pod | ✅ Yes | ❌ No |
hostPath | Same as Node | ✅ Yes | ✅ Yes (data stays on node) |
configMap / secret | Same as Pod | ✅ Yes | ❌ No (but source object persists) |
persistentVolumeClaim | Independent | ✅ Yes | ✅ Yes (PVC persists) |
2. emptyDir — Scratch Space
Created when a Pod is assigned to a node. Starts empty. Deleted when the Pod is removed. Shared between all containers in the Pod.
spec:
volumes:
- name: scratch
emptyDir: {} # Default: node disk
- name: cache
emptyDir:
medium: Memory # tmpfs (RAM-backed) — faster, counts against memory limits
sizeLimit: 500Mi # Evict Pod if exceeded
containers:
- name: app
volumeMounts:
- name: scratch
mountPath: /tmp/work
- name: sidecar
volumeMounts:
- name: scratch
mountPath: /shared # Same volume, different mountPath — shared data!
Use Cases
| Use Case | Configuration |
|---|---|
| Sharing files between containers (sidecar pattern) | emptyDir: {} mounted in both containers |
| Scratch space for sorting/processing | emptyDir: {} |
| High-performance cache | emptyDir: {medium: Memory} (tmpfs) |
| Content from init container | Init container writes to emptyDir, app container reads |
medium: Memory creates a tmpfs. It's fast (RAM speed), but: (1) counts against the container's memory limit, (2) data lost on Pod eviction, (3) limited by sizeLimit. If the Pod exceeds sizeLimit, it's evicted. Use it for caches and temporary data only.
3. hostPath — Node Filesystem Access
Mounts a file or directory from the host node's filesystem into the Pod. The data persists on that specific node even after the Pod is deleted.
spec:
volumes:
- name: host-logs
hostPath:
path: /var/log # Path on the NODE
type: Directory # Must exist as a directory
containers:
- name: log-collector
volumeMounts:
- name: host-logs
mountPath: /host-logs
readOnly: true
hostPath Types
| Type | Behavior |
|---|---|
"" (empty) | No checks — mount whatever exists (or create) |
DirectoryOrCreate | Create directory if it doesn't exist (0755, kubelet user) |
Directory | Must already exist as a directory |
FileOrCreate | Create file if it doesn't exist |
File | Must already exist as a file |
Socket | Must be a Unix socket |
CharDevice | Must be a character device |
BlockDevice | Must be a block device |
hostPath is a major security risk. It gives Pods direct access to the node filesystem — an attacker can read /etc/shadow, /var/lib/kubelet, or escape the container entirely by mounting /. In CKS context: Pod Security Standards (Restricted profile) prohibit hostPath. Only use it for system-level DaemonSets (logging, monitoring) with strict RBAC controls.
When hostPath Is Acceptable
- DaemonSets for log collection — mount
/var/logread-only - Node monitoring agents — mount
/proc,/sysread-only - Container runtime socket — mount
/var/run/containerd/containerd.sock - Static Pod manifests —
/etc/kubernetes/manifests
4. Projected Volumes — Multiple Sources, One Mount
A projected volume combines multiple volume sources into a single directory. Instead of mounting 4 separate volumes, you mount one that contains files from all of them.
spec:
volumes:
- name: pod-info
projected:
sources:
- configMap:
name: app-config
items:
- key: app.properties
path: config/app.properties
- secret:
name: db-creds
items:
- key: password
path: secrets/db-password
- downwardAPI:
items:
- path: metadata/labels
fieldRef:
fieldPath: metadata.labels
- serviceAccountToken:
path: token
expirationSeconds: 3600
audience: api-server
containers:
- name: app
volumeMounts:
- name: pod-info
mountPath: /etc/pod-info
readOnly: true
# Result in container: /etc/pod-info/ ├── config/app.properties (from ConfigMap) ├── secrets/db-password (from Secret) ├── metadata/labels (from Downward API) └── token (ServiceAccount token)
Sources You Can Project
| Source | What It Provides |
|---|---|
configMap | ConfigMap keys as files |
secret | Secret keys as files (tmpfs) |
downwardAPI | Pod metadata (labels, annotations, resources) |
serviceAccountToken | Bound SA token (audience, expiry — replaces legacy tokens) |
BoundServiceAccountTokenVolume.
/var/run/secrets/kubernetes.io/serviceaccount/token inside a Pod, that's a projected volume with a bound SA token. Understanding this is key for CKS (token security) and debugging Pod authentication issues.
5. Generic Ephemeral Volumes (K8s 1.23+)
Like emptyDir but with PVC-backed storage: you get a dynamically provisioned volume that's automatically deleted when the Pod is removed. Useful when you need more storage features (specific StorageClass, size guarantees) but don't want to manage the PVC lifecycle separately.
spec:
volumes:
- name: scratch-pvc
ephemeral:
volumeClaimTemplate:
metadata:
labels:
type: scratch
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 10Gi
containers:
- name: etl
volumeMounts:
- name: scratch-pvc
mountPath: /work
Ephemeral vs emptyDir vs PVC
| Aspect | emptyDir | Generic Ephemeral | PVC (Persistent) |
|---|---|---|---|
| Storage backend | Node disk or tmpfs | Dynamic PV (CSI driver) | Dynamic or static PV |
| Size guarantee | ❌ (sizeLimit is soft) | ✅ (actual provisioned volume) | ✅ |
| Performance class | Whatever the node has | Configurable (StorageClass) | Configurable |
| Lifecycle | Deleted with Pod | Deleted with Pod (auto) | Independent (survives Pod) |
| Use case | Small scratch space, sharing data | Large temp storage (ETL, builds) | Persistent data (databases) |
6. Quick Reference: All Primitive Volume Types
| Type | Source | Mutable? | Common Use |
|---|---|---|---|
emptyDir | Node disk or RAM | ✅ | Scratch space, container sharing |
hostPath | Node filesystem | ✅ | DaemonSets (logs, /proc, sockets) |
configMap | ConfigMap object | Read-only (auto-updates) | Config files |
secret | Secret object (tmpfs) | Read-only (auto-updates) | Credentials, TLS certs |
downwardAPI | Pod metadata | Read-only (updates on label change) | Pod identity info |
projected | Multiple sources combined | Read-only | SA tokens + config + secrets |
ephemeral | Dynamic PV (auto-delete) | ✅ | Large scratch, CI/CD, ETL |
persistentVolumeClaim | PVC → PV | ✅ | Databases, stateful workloads |
Summary
| Concept | Key Point |
|---|---|
| Volumes | Defined in spec.volumes, mounted in containers[].volumeMounts |
| emptyDir | Empty on creation, deleted with Pod, shared between containers |
| emptyDir Memory | tmpfs (RAM-backed), counts against memory limits |
| hostPath | Node filesystem — security risk, node-tied, use only for system DaemonSets |
| Projected | Combines ConfigMap + Secret + DownwardAPI + SA token in one mount |
| Ephemeral | Dynamic PVC created/deleted with Pod — large scratch with StorageClass |
| SA tokens | Projected bound tokens (short-lived, audience-bound) are the modern default |
📝 Quiz: Volume Primitives
Q1: An app container writes a file to /data. The container crashes and is restarted by kubelet. Is the file still there?
/data is backed by an emptyDir volume: yes, the file survives container restarts (emptyDir lives as long as the Pod). If it's just the container's writable layer (no volume): no, the file is lost on restart.Q2: You set emptyDir: {medium: Memory, sizeLimit: 100Mi}. The app writes 150Mi. What happens?
Q3: A Pod with a hostPath volume is rescheduled to a different node. What happens to the data?
Q4: What's the security concern with hostPath: {path: /}?
/etc/shadow (passwords), /var/lib/kubelet (secrets of all Pods), kubelet credentials, and SSH keys. This is essentially a container escape — the attacker has root on the node. Pod Security Standards (Restricted) prohibit this entirely.Q5: What does a projected volume with serviceAccountToken provide that the legacy SA token mount doesn't?
expirationSeconds (default 1h), automatically rotated by kubelet. Legacy tokens never expire. (2) Audience-bound — token is valid only for a specific audience (API server, Vault, etc.). (3) Object-bound — invalidated if the Pod is deleted. Legacy tokens remain valid forever even after Pod deletion.Q6: When would you use a generic ephemeral volume instead of emptyDir?