Volume #
In traditional Linux containerization (like plain Docker), the volume concept maps directories from the host into the container so data isn’t lost when the container stops. However, in the dynamic, distributed Kubernetes orchestration ecosystem, volume management needs a much more mature abstraction. Pods in Kubernetes are transient; they can be destroyed and rescheduled to different physical worker nodes at any time by the control plane.
To tackle this challenge, Kubernetes provides the Volume object. A Volume in Kubernetes is defined as part of the Pod’s lifecycle, not the container’s. This means the volume survives as long as the Pod is alive, regardless of how many times the containers inside crash or restart. Additionally, Kubernetes supports dozens of storage drivers — from local memory (RAM) to managed cloud storage, network file systems (NFS), and cluster metadata injection. This article dissects in depth how volumes work in Kubernetes, the local volume types and their configuration, the evolution toward CSI (Container Storage Interface), advanced layout patterns, and the configuration anti-patterns that often occur in production.
How Volumes Work in Pods: The Mounting Architecture #
In Kubernetes, volumes are defined at the Pod spec level (spec.volumes) as a storage provisioning declaration. Once the storage provider is defined, the containers inside that Pod must explicitly mount the volume into their local filesystems using the spec.containers[*].volumeMounts block.
This mechanism lets us share the same storage folder directly between containers in the same Pod.
Here’s a diagram of the bind-mount volume architecture from the physical host system into the container namespaces:
flowchart TD
subgraph PodSpec["Pod Spec"]
PV_Def["volumes:\n- name: shared-vol\n emptyDir: {}"]
end
subgraph Containers["Containers Inside the Pod"]
C1["Main Container (web-app)\nvolumeMounts:\n- name: shared-vol\n mountPath: /usr/share/nginx/html"]
C2["Sidecar Container (log-watcher)\nvolumeMounts:\n- name: shared-vol\n mountPath: /var/log/nginx"]
end
subgraph NodeHost["Worker Node Filesystem (Host Filesystem)"]
HostDir["/var/lib/kubelet/pods/{pod-id}/volumes/kubernetes.io~emptyDir/shared-vol"]
end
PV_Def -. Provides the local directory .-> HostDir
C1 -. Bind mounts the container path to the host .-> HostDir
C2 -. Bind mounts the container path to the host .-> HostDirExample Production YAML Manifest Sharing a Volume: #
apiVersion: v1
kind: Pod
metadata:
name: multi-container-volume-pod
namespace: production
spec:
volumes:
- name: shared-data-vol
emptyDir: {} # Provides empty space on the node
containers:
- name: web-server
image: nginx:1.25
volumeMounts:
- name: shared-data-vol
mountPath: /usr/share/nginx/html # Mounts to the Nginx HTML directory
- name: asset-downloader
image: alpine:3.18
command: ["sh", "-c", "wget -O /app/index.html http://example.com/index.html && sleep 3600"]
volumeMounts:
- name: shared-data-vol
mountPath: /app # Mounts to the download directory
Under the hood, the Kubelet manages this process by creating a physical directory in the worker node filesystem (under /var/lib/kubelet/pods/{pod-uid}/volumes/). When the container runs, the container runtime performs a Linux bind mount operation from that host directory into the container’s mount namespace.
Local Ephemeral Volume Types #
Local volumes only use the hardware capacity (disk or RAM) physically attached to the worker node where the Pod runs. Data in these volumes is guaranteed safe as long as the Pod doesn’t move nodes.
1. emptyDir: Temporary Empty Space
#
As briefly covered in the previous article, emptyDir is an empty directory created when the Pod starts. By default, emptyDir is created on the worker node’s storage medium (the node’s root SSD or HDD).
We can change its behavior by configuring the storage medium:
medium: Memory: Creates the directory in RAM (tmpfs). Very useful for applications needing high-throughput, low-latency data caching.sizeLimit: The hard size limit of the volume. The Kubelet periodically monitors this folder’s size. If the container writes data beyondsizeLimit, the Kubelet evicts the Pod to save the worker node’s memory.
2. hostPath: Direct Access to the Worker Node Filesystem
#
hostPath maps files or directories from the worker node’s OS filesystem directly into the container. This is a very powerful and equally very dangerous volume type.
Valid Production hostPath Use Cases: #
- Monitoring DaemonSet containers (like Prometheus Node Exporter) that need to read host hardware status at
/procor/sys. - Log collector containers (like Fluent Bit) that need to read the worker node OS logs at
/var/log. - Connecting the container runtime socket (
/var/run/docker.sockor/run/containerd/containerd.sock) to image-building containers (CI/CD runners).
Valid hostPath Types: #
DirectoryOrCreate ➔ Creates the directory on the host if it doesn't exist.
Directory ➔ The host directory must already exist (fails otherwise).
FileOrCreate ➔ Creates an empty file on the host if it doesn't exist.
File ➔ The host file must already exist.
Socket ➔ An existing Unix socket on the host.
[!CAUTION] Never use
hostPathfor databases or regular stateless business applications. If we deploy an app with a 3-replica Deployment usinghostPath: /data, and the Pod moves to another worker node due to hardware failure, the new Pod on worker node B can’t access data written by the old Pod on worker node A. Additionally,hostPathviolates security isolation because a malicious container can illegally modify the worker host OS.
Configuration and Metadata Volume Types #
Kubernetes has a unique ability to present system configuration, secrets, or internal cluster data as plain text files inside containers.
1. configMap and secret Volumes
#
Instead of baking configuration files (like nginx.conf or config.yaml) directly into the docker image, we can store them as ConfigMap/Secret objects in Kubernetes, then mount them as file volumes.
apiVersion: v1
kind: Pod
metadata:
name: config-mounted-pod
spec:
volumes:
- name: config-vol
configMap:
name: web-nginx-config # Pulls data from the ConfigMap object
defaultMode: 0640 # Sets file permissions (owner read-write, group read)
items:
- key: custom-nginx.conf
path: nginx.conf # Presented as the nginx.conf file in the container
containers:
- name: web
image: nginx:1.25
volumeMounts:
- name: config-vol
mountPath: /etc/nginx/conf.d
Hot Update Reload Mechanism: #
When the ConfigMap or Secret object is updated in the Kubernetes API, the Kubelet on the worker node detects the change. Within about 60-120 seconds, the Kubelet automatically updates the file contents inside the container via symlink updates. The container doesn’t need to be restarted to get the new files, as long as our application can detect file changes (hot reload), like Nginx with the SIGHUP signal.
2. downwardAPI Volumes
#
The Downward API lets containers read their own metadata without querying the Kubernetes API Server. The Kubelet writes that information into static text files inside the container.
volumes:
- name: pod-metadata-vol
downwardAPI:
items:
- path: "pod_name"
fieldRef:
fieldPath: metadata.name
- path: "pod_namespace"
fieldRef:
fieldPath: metadata.namespace
- path: "cpu_limit"
resourceFieldRef:
containerName: web
resource: limits.cpu
This is very useful for distributed clustering applications (like Elasticsearch) that need to know their own physical Pod name to join a master cluster.
3. projected Volumes
#
The projected volume acts as a consolidator merging several volume sources (like Secrets, ConfigMaps, DownwardAPI, and Service Account Tokens) into one single directory inside the container.
volumes:
- name: all-in-one-projected
projected:
sources:
- secret:
name: tls-certs
- configMap:
name: app-properties
- downwardAPI:
items:
- path: "labels"
fieldRef:
fieldPath: metadata.labels
This makes our YAML manifest structure much cleaner because the container only needs to write one volumeMounts line to access all those data types.
Storage Evolution: Decoupling via CSI (Container Storage Interface) #
In the early days of Kubernetes, the code to communicate with cloud storage providers (like AWS EBS, GCP PD, or VMware disks) was written directly into Kubernetes’ core binary (in-tree drivers).
- Problem: If AWS released a new storage feature, the Kubernetes team had to release a Kubernetes update to include that driver. Likewise, if there was a security bug in the Azure Disk driver, the entire Kubernetes controller had to be patched.
The Birth of the CSI Standard (Container Storage Interface) #
To separate the Kubernetes core development lifecycle from storage drivers, Kubernetes adopted the CSI (Container Storage Interface) standard.
[In-Tree Storage (Old Way - Deprecated)]
Kubernetes Core Binary ➔ [ AWS EBS Driver ] ➔ Cloud API
[CSI Architecture (Modern Way)]
Kubernetes Core Binary ➔ [ CSI Standard API ] ➔ [ CSI Driver (AWS/GCP/Ceph) ] ➔ Cloud API
CSI is an industry-standard specification allowing third-party storage vendors to develop their own storage drivers independently as separate containers deployed inside the cluster.
- How does it work? Storage vendors deploy the CSI Controller on the control plane and the CSI Node plugin as a DaemonSet on every worker node.
- Modern Usage: We no longer define cloud driver details at the Pod spec level. We just point to the driver name via the StorageClass:
# Example of calling a CSI Driver directly (only for static PVs)
volumes:
- name: cloud-disk
csi:
driver: ebs.csi.aws.com # Calls the AWS EBS CSI driver
volumeHandle: vol-09218bc109f # Physical volume ID in the cloud
Advanced Volume Layout Patterns #
1. Using subPath to Share Volumes
#
Sometimes we only want to mount a specific sub-directory of a volume, not the entire volume contents. subPath lets us isolate the mount path to a specific directory inside the volume.
spec:
containers:
- name: mysql
image: mysql:8.0
volumeMounts:
- name: database-storage-vol
mountPath: /var/lib/mysql
subPath: mysql-data # Only mounts the 'mysql-data' folder from the volume
- name: pgsql
image: postgres:15
volumeMounts:
- name: database-storage-vol
mountPath: /var/lib/postgresql/data
subPath: postgres-data # Shares the same volume in separate folders
volumes:
- name: database-storage-vol
persistentVolumeClaim:
claimName: shared-disk-pvc
[!CAUTION] In production, sharing one physical disk (RWO) for two separate databases (MySQL & PostgreSQL) using
subPathis highly discouraged because it triggers I/O transaction bottlenecks and increases the risk of total data loss if that disk fails. UsesubPathonly for log folders or non-critical assets.
2. Using subPathExpr for Dynamic Directories
#
If we run many Pod replicas under a StatefulSet and want to separate storage directories automatically by Pod name, we can use environment expressions via subPathExpr:
spec:
containers:
- name: log-writer
image: alpine
command: ["sh", "-c", "while true; do echo log >> /logs/app.log; sleep 5; done"]
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name # Injects the Pod name into the env var
volumeMounts:
- name: logs-vol
mountPath: /logs
subPathExpr: $(POD_NAME) # Folder name automatically matches the Pod name
Volume Usage Anti-Patterns & Their Solutions #
Here are three volume configuration mistakes that most often cause performance problems or security gaps in production:
Anti-Pattern 1: Using hostPath to Store Databases on Regular Deployments #
Assuming the worker node’s local host directory is safe for long-term database data storage.
ANTI-PATTERN: hostPath: { path: "/data/db" } for a Postgres Database
// WHAT WE DO:
- Deploy PostgreSQL using a Deployment (replicas: 1) with a hostPath volume pointing to /data/db.
// THE CONSEQUENCES IN PRODUCTION:
- When worker-node-1 (where the Pod runs) undergoes maintenance restart, the scheduler moves the Pod to worker-node-2.
- The PostgreSQL Pod starts on worker-node-2, looking for the /data/db folder on that node.
- Because that folder is empty (or holds stale data from other tests), our database starts
a brand new process from scratch (*fresh database*). Production transaction data stored on worker-node-1 is lost/stuck there.
✓ THE RIGHT SOLUTION:
- Use the `persistentVolumeClaim` volume type backed by a StorageClass with network storage
or cloud storage (like AWS EBS, GCP PD) so the disk can be dynamically reattached to the new worker node.
Anti-Pattern 2: Ignoring the defaultMode Setting on Secret Volumes #
Letting sensitive encrypted files or private SSH keys mount into containers with overly open default permissions.
ANTI-PATTERN: Mounting an SSH Key Secret Without defaultMode
// WHAT WE DO:
- Deploy a payment-processing Pod mounting a Secret containing an SSH private key.
- Ignore the `defaultMode` setting in the volume spec.
// THE CONSEQUENCES IN PRODUCTION:
- Loose Security: The private key file mounts with default `0644` permissions (readable by anyone).
- Application Rejection: Many modern cryptography libraries (like OpenSSH) refuse to process private
keys if the file permissions are too loose, triggering the error:
`Permissions 0644 for id_rsa are too open. It is required that your private key files are NOT accessible by others`.
✓ THE RIGHT SOLUTION:
- Always set the strictest permissions (`0400` - read-only for the owner) on sensitive credential files:
volumes:
- name: ssh-key-vol
secret:
secretName: payment-ssh-key
defaultMode: 0400 # Only the process owner may read
Anti-Pattern 3: Using subPath on ConfigMap/Secret Volumes, Killing Hot Updates #
Using the subPath property to place a single configuration file into a container folder that already contains other files.
ANTI-PATTERN: volumeMounts: [ { name: "config", mountPath: "/etc/nginx/nginx.conf", subPath: "nginx.conf" } ]
// WHAT WE DO:
- Mount the `nginx.conf` file from a ConfigMap into the `/etc/nginx/` directory using `subPath`
so other files in that directory aren't overwritten.
// THE CONSEQUENCES IN PRODUCTION:
- Dead Hot Reload Feature: When we update the ConfigMap in the API Server, the Kubelet never
updates the `nginx.conf` file contents inside the container.
- Why? Because the Linux bind mount for a single file is static and locks that file's inode
since startup. The Kubelet can't swap directory symlinks to update the data,
forcing us to manually restart the Pod for every configuration change.
✓ THE RIGHT SOLUTION:
- Don't use `subPath` if our configuration needs dynamic changes (*hot reload*).
- Mount the entire ConfigMap to a separate directory, then create a symlink from inside the container,
or use a dedicated sub-directory for extra configuration (like `/etc/nginx/conf.d/`).
Summary #
- Pod-Tied Lifecycle — Kubernetes volumes live as long as the Pod lives; container data in the volume isn’t lost even if the container restarts repeatedly.
- Sharing Volumes Between Containers — Get the sidecar pattern benefit by mounting the same volume (
emptyDir) to several different containers in one Pod.- hostPath Only for System Agents — Avoid
hostPathfor business applications; limit its use to DaemonSet log forwarders or node monitoring.- ConfigMap & Secret Injection — Present centralized configuration as files in containers; leverage automatic symlink updates without needing Pod restarts.
- Downward API for Identity — Use the Downward API to inject metadata like Pod name and namespace into application containers for clustering purposes.
- The Modern CSI Standard — All cloud storage interaction is managed externally through the Container Storage Interface (CSI), freeing clusters from built-in driver dependencies.
- Use subPath Carefully — Leverage
subPathto separate storage directories on a shared volume, but be aware of losing ConfigMap hot update capability.- Lock Credentials via defaultMode — Protect sensitive credentials in Secret volumes by setting strict
0400access permissions to prevent security exploits.
← Previous: Ephemeral vs Persistent Storage Next: Storage Problems in Distributed Systems →