In this episode, we'll discuss Kubernetes Volumes for persistent data storage. We'll learn about different volume types, how to mount volumes in Pods, and best practices for data persistence.

Note
If you want to read the previous episode, you can click the Episode 20 thumbnail below
In the previous episode, we learned about Multi-Container Pods and design patterns for running multiple containers together. In episode 21, we'll discuss Volumes, the mechanism for persisting data in Kubernetes.
Note: Here I'll be using a Kubernetes Cluster installed through K3s.
By default, container filesystems are ephemeral - when a container restarts, all data is lost. Volumes solve this problem by providing persistent storage that survives container restarts and can be shared between containers in a Pod.
A Volume is a directory accessible to containers in a Pod. Unlike the ephemeral container filesystem, volumes persist data beyond container restarts and can be shared between multiple containers.
Think of volumes like external hard drives - while your computer's internal storage is wiped when you reinstall the OS, an external drive keeps your data safe. Similarly, volumes preserve data when containers restart or crash.
Key characteristics of Volumes:
Volumes solve several critical storage challenges:
Without volumes, you would lose all data every time a container restarts, making it impossible to run stateful applications like databases.
Understanding volume lifecycle is crucial:
Most volumes are tied to Pod lifecycle:
Some volumes persist beyond Pod lifecycle:
Kubernetes supports many volume types for different use cases.
Temporary storage that exists as long as the Pod exists.
Use cases:
Example:
apiVersion: v1
kind: Pod
metadata:
name: emptydir-pod
spec:
containers:
- name: writer
image: busybox:1.36
command:
- sh
- -c
- while true; do date >> /data/log.txt; sleep 5; done
volumeMounts:
- name: shared-data
mountPath: /data
- name: reader
image: busybox:1.36
command:
- sh
- -c
- tail -f /data/log.txt
volumeMounts:
- name: shared-data
mountPath: /data
volumes:
- name: shared-data
emptyDir: {}emptyDir with memory:
volumes:
- name: cache
emptyDir:
medium: Memory
sizeLimit: 128MiThis creates a tmpfs (RAM-backed filesystem) for high-performance temporary storage.
Mounts a file or directory from the host node's filesystem.
Use cases:
Warning
Warning: hostPath volumes are not portable across nodes and pose security risks. Use only when necessary.
Example:
apiVersion: v1
kind: Pod
metadata:
name: hostpath-pod
spec:
containers:
- name: app
image: nginx:1.25
volumeMounts:
- name: host-data
mountPath: /usr/share/nginx/html
volumes:
- name: host-data
hostPath:
path: /data/web
type: DirectoryOrCreatehostPath types:
DirectoryOrCreate - Create directory if it doesn't existDirectory - Must be existing directoryFileOrCreate - Create file if it doesn't existFile - Must be existing fileSocket - Must be existing Unix socketCharDevice - Must be existing character deviceBlockDevice - Must be existing block deviceMounts ConfigMap data as files in the container.
Use cases:
Example:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
app.conf: |
server {
listen 80;
server_name localhost;
}
database.conf: |
host=db.example.com
port=5432
---
apiVersion: v1
kind: Pod
metadata:
name: configmap-pod
spec:
containers:
- name: app
image: nginx:1.25
volumeMounts:
- name: config
mountPath: /etc/config
volumes:
- name: config
configMap:
name: app-configThat configuration will create file app.conf and database.conf inside the container at path /etc/config. To see them, you can enter the container with the command sudo kubectl exec -it configmap-pod -- sh and view the files with the command cat /etc/config/app.conf and cat /etc/config/database.conf.
Mount specific keys:
volumes:
- name: config
configMap:
name: app-config
items:
- key: app.conf
path: nginx.confMounts Secret data as files in the container.
Use cases:
Example:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
username: admin
password: secretpassword
---
apiVersion: v1
kind: Pod
metadata:
name: secret-pod
spec:
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: credentials
mountPath: /etc/secrets
readOnly: true
volumes:
- name: credentials
secret:
secretName: db-credentialsFiles created:
/etc/secrets/username (contains "admin")/etc/secrets/password (contains "secretpassword")References a PersistentVolumeClaim for durable storage.
Use cases:
Example:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Pod
metadata:
name: pvc-pod
spec:
containers:
- name: app
image: postgres:15
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: data-pvcWe'll cover PersistentVolumes and PersistentVolumeClaims in detail in the next episode.
Exposes Pod metadata as files.
Use cases:
Example:
apiVersion: v1
kind: Pod
metadata:
name: downwardapi-pod
labels:
app: myapp
version: v1.0
spec:
containers:
- name: app
image: busybox:1.36
command:
- sh
- -c
- while true; do cat /etc/podinfo/*; sleep 10; done
volumeMounts:
- name: podinfo
mountPath: /etc/podinfo
volumes:
- name: podinfo
downwardAPI:
items:
- path: name
fieldRef:
fieldPath: metadata.name
- path: namespace
fieldRef:
fieldPath: metadata.namespace
- path: labels
fieldRef:
fieldPath: metadata.labelsCombines multiple volume sources into a single directory.
Use cases:
Example:
apiVersion: v1
kind: Pod
metadata:
name: projected-pod
spec:
containers:
- name: app
image: nginx:1.25
volumeMounts:
- name: all-config
mountPath: /etc/config
volumes:
- name: all-config
projected:
sources:
- configMap:
name: app-config
- secret:
name: db-credentials
- downwardAPI:
items:
- path: pod-name
fieldRef:
fieldPath: metadata.nameCustomize how volumes are mounted in containers.
Prevent containers from modifying volume data:
volumeMounts:
- name: config
mountPath: /etc/config
readOnly: trueMount a specific file or subdirectory from a volume:
apiVersion: v1
kind: Pod
metadata:
name: subpath-pod
spec:
containers:
- name: app
image: nginx:1.25
volumeMounts:
- name: config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
volumes:
- name: config
configMap:
name: nginx-configThis mounts only the nginx.conf file, not the entire ConfigMap.
Use environment variables in subPath:
volumeMounts:
- name: data
mountPath: /var/data
subPathExpr: $(POD_NAME)
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.nameapiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: web-content-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
spec:
replicas: 1
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
volumeMounts:
- name: content
mountPath: /usr/share/nginx/html
volumes:
- name: content
persistentVolumeClaim:
claimName: web-content-pvcapiVersion: v1
kind: Pod
metadata:
name: multi-volume-app
spec:
containers:
- name: app
image: myapp:latest
volumeMounts:
# Application config from ConfigMap
- name: config
mountPath: /etc/app/config
readOnly: true
# Database credentials from Secret
- name: secrets
mountPath: /etc/app/secrets
readOnly: true
# Persistent data storage
- name: data
mountPath: /var/lib/app
# Temporary cache
- name: cache
mountPath: /tmp/cache
# Logs shared with sidecar
- name: logs
mountPath: /var/log/app
- name: log-shipper
image: fluent/fluentd:v1.16
volumeMounts:
- name: logs
mountPath: /var/log/app
readOnly: true
volumes:
- name: config
configMap:
name: app-config
- name: secrets
secret:
secretName: app-secrets
- name: data
persistentVolumeClaim:
claimName: app-data-pvc
- name: cache
emptyDir:
medium: Memory
sizeLimit: 256Mi
- name: logs
emptyDir: {}apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-pvcapiVersion: v1
kind: Pod
metadata:
name: init-volume-pod
spec:
initContainers:
- name: setup
image: busybox:1.36
command:
- sh
- -c
- |
echo "Downloading configuration..."
wget -O /config/app.conf https://config-server/app.conf
echo "Setup complete"
volumeMounts:
- name: config
mountPath: /config
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: config
mountPath: /etc/app
volumes:
- name: config
emptyDir: {}Different volumes support different access modes:
Volume can be mounted read-write by a single node:
accessModes:
- ReadWriteOnceMost common for block storage (AWS EBS, GCE PD).
Volume can be mounted read-only by many nodes:
accessModes:
- ReadOnlyManyUseful for shared configuration or static content.
Volume can be mounted read-write by many nodes:
accessModes:
- ReadWriteManyRequires network filesystem (NFS, CephFS, GlusterFS).
Volume can be mounted read-write by a single Pod:
accessModes:
- ReadWriteOncePodKubernetes 1.22+ feature for strict single-Pod access.
Problem: Data lost when Pod is deleted.
Solution: Use PersistentVolumeClaim for data that must survive Pod deletion:
# Bad: emptyDir for database
volumes:
- name: data
emptyDir: {}
# Good: PVC for database
volumes:
- name: data
persistentVolumeClaim:
claimName: db-pvcProblem: Containers can modify sensitive data.
Solution: Always mount secrets as read-only:
volumeMounts:
- name: secrets
mountPath: /etc/secrets
readOnly: trueProblem: Volume can't be mounted on multiple nodes.
Solution: Use ReadWriteMany for multi-node access:
accessModes:
- ReadWriteMany # For shared accessProblem: emptyDir can fill up node disk.
Solution: Set sizeLimit for emptyDir:
emptyDir:
sizeLimit: 1GiProblem: Not portable, security risks, node dependency.
Solution: Use PersistentVolumes instead:
# Avoid in production
hostPath:
path: /data
# Use instead
persistentVolumeClaim:
claimName: data-pvcChoose the right volume type for your use case:
# Temporary data
emptyDir: {}
# Configuration
configMap:
name: app-config
# Sensitive data
secret:
secretName: credentials
# Persistent data
persistentVolumeClaim:
claimName: data-pvcAlways limit emptyDir size:
emptyDir:
sizeLimit: 500MiMount volumes as read-only when possible:
volumeMounts:
- name: config
mountPath: /etc/config
readOnly: trueGroup related volumes logically:
volumeMounts:
# Configuration volumes
- name: app-config
mountPath: /etc/app
- name: nginx-config
mountPath: /etc/nginx
# Data volumes
- name: data
mountPath: /var/lib/app
# Temporary volumes
- name: cache
mountPath: /tmp/cacheSubPath can cause issues with updates:
# ConfigMap updates won't reflect with subPath
volumeMounts:
- name: config
mountPath: /etc/app/config.yml
subPath: config.yml # Blocks updatesMount entire volume when possible.
Add comments explaining volume usage:
volumes:
# Application configuration files
- name: config
configMap:
name: app-config
# Database credentials
- name: secrets
secret:
secretName: db-credentials
# Persistent application data
- name: data
persistentVolumeClaim:
claimName: app-data-pvcsudo kubectl get pod <pod-name> -o jsonpath='{.spec.volumes[*].name}'sudo kubectl describe pod <pod-name>Shows all volumes and their mounts.
sudo kubectl exec <pod-name> -- df -hsudo kubectl exec <pod-name> -- ls -la /path/to/mountCheck Pod events:
sudo kubectl describe pod <pod-name>Look for mount errors in events.
Check volume permissions and security context:
securityContext:
fsGroup: 1000
runAsUser: 1000Verify resource exists:
sudo kubectl get configmap <name>
sudo kubectl get secret <name>Check disk usage:
sudo kubectl exec <pod-name> -- df -hIncrease sizeLimit or clean up data.
In episode 21, we've explored Volumes in Kubernetes in depth. We've learned about different volume types, how to mount them in Pods, and best practices for data persistence.
Key takeaways:
Volumes are essential for running stateful applications in Kubernetes. By understanding different volume types and their use cases, you can design robust storage solutions for your applications.
Are you getting a clearer understanding of Volumes in Kubernetes? Keep your learning momentum going and look forward to the next episode!
Note
If you want to continue to episode 21.1 where we deep-dive into PersistentVolume and PersistentVolumeClaim, you can click the Episode 21.1 thumbnail below