Volumes & volume types
emptyDir, hostPath, and why pods need volumes.
Kill a container and everything it wrote to disk is gone. Not corrupted, not archived somewhere for later. Gone. Every container starts with a thin writable layer stacked on top of its read-only image, and the instant the container restarts, that layer gets thrown away and rebuilt fresh from the image. That's wonderful for keeping containers clean and identical everywhere they run. It's a disaster if your app just wrote a file it needs ten seconds from now.
Think of a container's own disk as the scratch paper a temp worker gets on day one. Handy during the shift, shredded the moment they clock out. A volume is a drawer built into the desk that stays put no matter who sits down. In Kubernetes a volume is storage you define at the Pod level (a Pod is one or more containers that always run together on the same machine) and mount into whichever containers need it. Its lifetime is tied to the Pod, not to any single container inside it. Hold onto that sentence. Everything below is a variation on it.
Why 'tied to the Pod' is the whole game
Two headaches disappear once storage outlives the container. A crash no longer wipes your data, because the kubelet (the Kubernetes agent running on every node) restarts the container in place and re-attaches the same volume. And containers in the same Pod can pass files to each other through a shared volume: a sidecar tailing the app's log file, an init container fetching data the app reads at startup. They mount one volume at different paths and see the exact same bytes.
emptyDir: shared scratch space
The simplest volume there is. An emptyDir is created empty when the Pod lands on a node and deleted when the Pod leaves that node. Good for a cache, temp files mid-computation, or handing data from one container to another. Here two containers in one Pod share a single emptyDir, each mounting it at a different path.
apiVersion: v1kind: Podmetadata:name: shared-scratchspec:containers:- name: appimage: busybox:1.36command: ["sh", "-c", "sleep 3600"]volumeMounts:- name: scratchmountPath: /data- name: sidecarimage: busybox:1.36command: ["sh", "-c", "sleep 3600"]volumeMounts:- name: scratchmountPath: /sharedvolumes:- name: scratchemptyDir: {}
Apply it, then describe the Pod. The Mounts and Volumes sections show you exactly what the kubelet wired up: the same volume mounted into both containers at two different paths.
Containers:app:Mounts:/data from scratch (rw)sidecar:Mounts:/shared from scratch (rw)Volumes:scratch:Type: EmptyDir (a temporary directory that shares a pod's lifetime)Medium:SizeLimit: <unset>
Now prove the sharing is real. Write a file from the app container and read it back from the sidecar.
kubectl exec shared-scratch -c app -- sh -c 'echo "hi from app" > /data/note.txt'kubectl exec shared-scratch -c sidecar -- cat /shared/note.txt
hi from app
No storage system, no provisioning, nothing to clean up afterward. Delete the Pod and the directory vanishes with it. You can also put an emptyDir in RAM by setting medium: Memory, which gives you a tmpfs (a filesystem that lives in memory instead of on disk). It's fast and wiped on reboot, but every byte it holds counts against the Pod's memory budget.
hostPath: borrowing the node's own disk
Sometimes you really do need a file from the machine itself: reading /var/log, reaching the container runtime socket, a monitoring agent that inspects the host. hostPath mounts a path from the node's filesystem straight into the Pod. The type field tells the kubelet what to expect (Directory means it must already exist, DirectoryOrCreate makes it if it's missing).
apiVersion: v1kind: Podmetadata:name: node-log-readerspec:containers:- name: readerimage: busybox:1.36command: ["sh", "-c", "tail -f /host/log/syslog"]volumeMounts:- name: varlogmountPath: /host/logreadOnly: truevolumes:- name: varloghostPath:path: /var/logtype: Directory
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODEnode-log-reader 1/1 Running 0 9s 10.244.2.7 worker-node-2 <none>
That NODE column is the catch, and it shows up on the exam and in real incidents. hostPath nails the Pod to whichever node holds that path, so if the Pod reschedules elsewhere the data simply isn't there. And it hands a container a door straight into the host: mount the node's root filesystem or the kubelet's credentials and one compromised container owns the machine. It's a well-worn container-escape route. Keep it readOnly, reserve it for node agents that genuinely need host access, and never point application data at it.
configMap and secret volumes
Config and secrets can arrive as files too. A configMap volume turns each key into a file under your mount path. A secret volume does the same for sensitive values and keeps them in tmpfs so they never land on node disk. Your app just reads /etc/config/settings and stays blind to where it came from, which is how you ship config without baking it into the image. One trap worth knowing: mount a single key with subPath and that file stops receiving live updates when you edit the configMap, because subPath breaks the symlink Kubernetes uses to swap in new content.
Two fields on these volumes are worth setting before they bite you. defaultMode is the permission Kubernetes stamps on every file it writes, given in octal. Leave it out and you get 0644, readable by everyone inside the container, which is how a private key ends up failing a hardening check. The second is availability: a configMap or secret volume is required by default, so if the object you named does not exist yet the kubelet cannot mount it and the Pod sits in ContainerCreating until someone creates it. Set optional: true when the file is a nice-to-have, and the Pod starts anyway with the mount directory simply empty.
volumes:- name: configconfigMap:name: app-configdefaultMode: 0400 # owner read-only; leave it out and you get 0644optional: true # missing configMap? start anyway, mount an empty dir
What the kubelet actually does, and how to check it
The kubelet does the physical wiring. When a Pod is assigned to a node, the kubelet stages most volumes on the node's disk first, under /var/lib/kubelet/pods/<pod-uid>/volumes/, with one subdirectory per volume type: an empty directory for emptyDir (kubernetes.io~empty-dir), written files for configMap and secret (kubernetes.io~configmap, kubernetes.io~secret). hostPath is the exception, because there is nothing to stage. The path you named on the node is used exactly as it is, so don't go hunting for a hostPath entry under that directory, you won't find one. The kubelet then hands the finished list of source paths to the container runtime, which bind-mounts each one into the container at the path you chose. When a mount misbehaves, two moves cover almost everything: kubectl describe pod to read the Mounts and Volumes sections you saw above, and kubectl exec into the container to list the actual files. If a file is missing, it's nearly always a name mismatch between volumeMounts and volumes, or a configMap that doesn't exist yet.
LAST SEEN TYPE REASON OBJECT MESSAGE12s Warning Evicted pod/cache-hog Usage of EmptyDir volume "cache" exceeds the limit "512Mi".
Try this
Go and see the staging directory for yourself on a lab cluster where you can get a shell on the node (minikube ssh, or docker exec -it <kind-node> bash). Ask the API for the Pod's UID, then list that Pod's volumes directory on the node. You get one entry for the emptyDir and one for the projected service account token every Pod gets. Add a hostPath volume to the Pod and look again: still no entry for it, because the host path is mounted straight in.
$ kubectl get pod shared-scratch -o jsonpath='{.metadata.uid}'9c1b0a4e-2f77-4a9d-9f0b-3d2c5e6a1b88# now on the node itself$ sudo ls /var/lib/kubelet/pods/9c1b0a4e-2f77-4a9d-9f0b-3d2c5e6a1b88/volumes/kubernetes.io~empty-dir kubernetes.io~projected
Takeaway
Volumes give pods files that outlive a container process. emptyDir dies with the pod; hostPath ties you to a node and its security risks.