Volumes & Volume Mounts
A container's filesystem is ephemeral — when the container restarts, everything written to disk is gone. Kubernetes Volumes solve this by attaching storage to a pod that outlives any individual container restart. Volumes also enable multiple containers in a pod to share data through a common directory. This guide covers the built-in volume types you'll use most often; PersistentVolumes (for durable, node-independent storage) are covered in the next guide.
Why Volumes?
Containers need external storage for two distinct reasons:
- Durability across restarts: log files, pid files, cache files, lock files — anything a container writes that should survive a crash and restart
- Sharing between containers: sidecar patterns (a log-shipper reading from the same directory as the app), init containers writing config that the main container reads
A Volume's lifetime is tied to the pod, not the container. If a container restarts due to a crash, the volume data is preserved. If the pod is deleted and rescheduled, ephemeral volumes like emptyDir start fresh — for persistent data that survives pod deletion, use PersistentVolumes.
Volume Anatomy
Every volume requires two entries: a declaration in spec.volumes giving the volume a name and type, and one or more volumeMounts inside container specs linking that name to a filesystem path.
spec:
volumes: # declare volumes at the pod level
- name: shared-data # any name you choose
emptyDir: {} # volume type
- name: app-config
configMap:
name: my-config # reference an existing ConfigMap
containers:
- name: app
image: my-app:latest
volumeMounts: # mount inside this container
- name: shared-data # must match a volume name above
mountPath: /data # where it appears in the container
- name: app-config
mountPath: /etc/config
readOnly: true # optional: read-only mount
emptyDir
emptyDir starts as an empty directory when the pod is scheduled to a node. It lives for the lifetime of the pod. Multiple containers in the same pod can mount the same emptyDir volume and read/write to it concurrently.
apiVersion: v1
kind: Pod
metadata:
name: cache-pod
spec:
volumes:
- name: cache
emptyDir: {} # default: stored on node disk
- name: ramdisk
emptyDir:
medium: Memory # stored in RAM (tmpfs) — faster, uses node memory
sizeLimit: 256Mi # cap RAM usage
containers:
- name: app
image: my-app:latest
volumeMounts:
- name: cache
mountPath: /var/cache/app
- name: ramdisk
mountPath: /tmp/fast
Use medium: Memory for high-performance scratch space — temp files, shared memory IPC. The data counts against the container's memory limit. Without a sizeLimit, the ramdisk can grow until it exhausts node memory.
hostPath
hostPath mounts a file or directory from the node's filesystem into the pod. The node path must exist before the pod starts (unless type: DirectoryOrCreate is used).
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
type: Socket # File, Directory, Socket, CharDevice, BlockDevice
# DirectoryOrCreate / FileOrCreate: create if missing
A container that mounts hostPath: / or /etc can read and write the entire node filesystem. Mounting /var/run/docker.sock gives full Docker daemon access, which is equivalent to root on the node. Use hostPath only for system-level DaemonSets (log collectors, monitoring agents) that genuinely need node-level access, and restrict it with PodSecurity policies.
ConfigMap & Secret Volumes
ConfigMaps and Secrets can be projected into the filesystem as files, not just environment variables. This is useful for config files, TLS certificates, and credentials that apps expect to read from disk.
apiVersion: v1
kind: Pod
metadata:
name: configured-app
spec:
volumes:
- name: config
configMap:
name: nginx-config
items: # optional: select specific keys
- key: nginx.conf # ConfigMap key
path: nginx.conf # filename in the volume
- name: tls-certs
secret:
secretName: tls-secret
defaultMode: 0400 # file permissions (octal)
containers:
- name: nginx
image: nginx:1.27
volumeMounts:
- name: config
mountPath: /etc/nginx/conf.d
- name: tls-certs
mountPath: /etc/nginx/tls
readOnly: true
When the ConfigMap or Secret is updated, Kubernetes eventually reflects the change in the mounted files — typically within the kubelet's sync period (default 60s). Environment variables from ConfigMaps/Secrets do not update automatically; volume mounts do.
Projected Volumes
A projected volume combines multiple sources into a single mount directory — a ConfigMap, a Secret, a ServiceAccount token, and the pod's downward API data, all accessible under one path:
volumes:
- name: all-in-one
projected:
sources:
- configMap:
name: app-config
- secret:
name: app-secret
- serviceAccountToken:
path: token
expirationSeconds: 3600 # auto-rotated token
- downwardAPI:
items:
- path: pod-name
fieldRef:
fieldPath: metadata.name
Multi-Container Sharing
The most common use of emptyDir is sharing a directory between a main container and a sidecar. The sidecar pattern below has a log-shipper reading from a shared log directory:
spec:
volumes:
- name: logs
emptyDir: {}
containers:
- name: app
image: my-app:latest
volumeMounts:
- name: logs
mountPath: /var/log/app
- name: log-shipper
image: fluent/fluent-bit:latest
volumeMounts:
- name: logs
mountPath: /var/log/app # same path, same volume name
env:
- name: OUTPUT_HOST
value: elasticsearch.logging:9200
subPath Mounts
By default, mounting a volume replaces the entire target directory. subPath lets you mount a single key from a ConfigMap (or a subdirectory) into an existing directory without overwriting the rest of its contents:
volumeMounts:
- name: config
mountPath: /etc/nginx/nginx.conf # mount a single file
subPath: nginx.conf # key from the ConfigMap
# Without subPath: /etc/nginx/ would be replaced entirely by the ConfigMap
# With subPath: only nginx.conf is added/replaced; other files in /etc/nginx/ are untouched
Files mounted with subPath do not reflect ConfigMap/Secret changes automatically. The whole-directory mount (without subPath) will update within the sync period. If you need live config updates, avoid subPath and use the full directory mount instead.
Init Container Pattern
Init containers run to completion before the main container starts. A common pattern uses an emptyDir to pass prepared data from an init container to the main container — downloading assets, rendering config templates, seeding a database schema:
spec:
volumes:
- name: config
emptyDir: {}
initContainers:
- name: init-config
image: alpine:3.20
command: ["/bin/sh", "-c"]
args:
- |
wget -O /config/settings.json https://config-service/settings
volumeMounts:
- name: config
mountPath: /config
containers:
- name: app
image: my-app:latest
volumeMounts:
- name: config
mountPath: /app/config # reads settings.json written by init container
kubectl Commands
# Describe a pod and see its volume definitions and mounts
kubectl describe pod my-pod
# Verify a volume is mounted inside the container
kubectl exec my-pod -- ls /data
kubectl exec my-pod -- cat /etc/config/nginx.conf
# Check what's in an emptyDir (from inside the pod)
kubectl exec my-pod -c app -- df -h /var/cache
# Verify a ConfigMap volume is up to date
kubectl exec my-pod -- cat /etc/config/settings.json
# Debug mount issues — look for "Unhealthy" events
kubectl describe pod my-pod | grep -A 20 Events
# List volumes on a running pod (jsonpath)
kubectl get pod my-pod \
-o jsonpath='{.spec.volumes[*].name}'