Volumes and Persistent Storage
Containers are ephemeral — when a container restarts, its filesystem is wiped clean. Volumes solve this by providing storage that persists beyond the container lifecycle, and Persistent Volumes take it further by providing storage that survives Pod deletion and rescheduling.
The Storage Problem
Without volumes:
- Container filesystem is temporary — data lost on restart
- Containers in the same Pod can’t share files
- No way to persist database files, logs, or uploaded content
Volume Types Overview
| Category | Volume Type | Lifetime | Use Case |
|---|---|---|---|
| Ephemeral | emptyDir |
Same as Pod | Shared temp storage between containers |
| Node-level | hostPath |
Same as Node | Access node files (dev only) |
| Persistent | PV + PVC | Independent of Pod | Databases, file uploads |
| Config | configMap, secret |
Same as the resource | Configuration files |
| Cloud | awsElasticBlockStore, gcePersistentDisk, azureDisk |
Independent | Cloud-native persistent storage |
emptyDir — Temporary Shared Storage
An emptyDir volume is created when a Pod is assigned to a node and exists as long as the Pod is running. Perfect for sharing files between containers in the same Pod.
apiVersion: v1
kind: Pod
metadata:
name: shared-storage
spec:
containers:
- name: writer
image: busybox
command: ["sh", "-c", "while true; do date >> /data/log.txt; sleep 5; done"]
volumeMounts:
- name: shared-data
mountPath: /data
- name: reader
image: busybox
command: ["sh", "-c", "tail -f /data/log.txt"]
volumeMounts:
- name: shared-data
mountPath: /data
volumes:
- name: shared-data
emptyDir: {}
The writer container writes timestamps, the reader container reads them — both share the same volume.
kubectl apply -f shared-storage.yaml
kubectl logs shared-storage -c reader -f
hostPath — Node-Level Storage
Mounts a file or directory from the host node’s filesystem into the Pod. Generally not recommended for production because Pods can be scheduled on different nodes.
volumes:
- name: host-data
hostPath:
path: /var/log/host
type: Directory # DirectoryOrCreate, File, FileOrCreate
Warning: hostPath is a security risk in production. It gives Pods access to the node’s filesystem. Use it only for specific use cases like log collection or node monitoring.
Persistent Volumes (PV) and Persistent Volume Claims (PVC)
For storage that must survive Pod deletions, rescheduling, and node failures, use the PV/PVC system.
The Three Components
Administrator Developer Kubernetes
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Creates │ │ Creates │ │ Binds │
│ PV (or │◄────────────►│ PVC │◄────────────►│ PVC to │
│ StorageClass)│ │ (request) │ │ matching │
└──────────┘ └──────────────┘ │ PV │
└──────────┘
| Component | Who Creates It | What It Does |
|---|---|---|
| PersistentVolume (PV) | Admin (or dynamic provisioner) | Represents actual storage (like an NFS share or cloud disk) |
| PersistentVolumeClaim (PVC) | Developer | A request for storage (size, access mode) |
| StorageClass | Admin | Defines how storage is dynamically provisioned |
Static Provisioning
Admin creates the PV, developer creates the PVC, Kubernetes binds them:
# pv.yaml — Created by admin
apiVersion: v1
kind: PersistentVolume
metadata:
name: my-pv
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
hostPath: # For Minikube/dev only
path: /mnt/data
---
# pvc.yaml — Created by developer
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi # Requests 5Gi from available PVs
kubectl apply -f pv.yaml
kubectl apply -f pvc.yaml
# Check binding
kubectl get pv
kubectl get pvc
Output:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM
my-pv 10Gi RWO Retain Bound default/my-pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES
my-pvc Bound my-pv 10Gi RWO
Using a PVC in a Pod
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
value: mysecretpassword
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: my-pvc
Access Modes
| Mode | Abbreviation | Description |
|---|---|---|
| ReadWriteOnce | RWO | Single node can mount as read-write |
| ReadOnlyMany | ROX | Multiple nodes can mount as read-only |
| ReadWriteMany | RWX | Multiple nodes can mount as read-write |
| ReadWriteOncePod | RWOP | Single Pod can mount as read-write (K8s 1.29+) |
Reclaim Policies
What happens to the PV when the PVC is deleted:
| Policy | Behavior |
|---|---|
| Retain | PV is kept (data preserved). Admin must manually reclaim. |
| Delete | PV and underlying storage are deleted automatically. |
| Recycle | Deprecated. Use dynamic provisioning instead. |
Dynamic Provisioning with StorageClass
Instead of manually creating PVs, define a StorageClass and let Kubernetes automatically provision storage when a PVC is created.
# storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: kubernetes.io/aws-ebs # Cloud-specific
parameters:
type: gp3
fsType: ext4
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
# pvc.yaml — References the StorageClass
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: fast-storage
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd # References the StorageClass
resources:
requests:
storage: 20Gi
When this PVC is created, Kubernetes automatically provisions a 20Gi gp3 EBS volume.
Minikube Default StorageClass
Minikube comes with a default StorageClass:
kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE
standard (default) k8s.io/minikube-hostpath Delete Immediate
You can use it without specifying storageClassName:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
# No storageClassName — uses default
Hands-On: Persistent Data with Minikube
# persistent-app.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: counter-app
spec:
replicas: 1
selector:
matchLabels:
app: counter
template:
metadata:
labels:
app: counter
spec:
containers:
- name: counter
image: busybox
command: ["sh", "-c"]
args:
- |
count=$(cat /data/count 2>/dev/null || echo 0)
while true; do
count=$((count + 1))
echo $count > /data/count
echo "Count: $count"
sleep 5
done
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: app-data
# Apply
kubectl apply -f persistent-app.yaml
# Watch the counter
kubectl logs -f deployment/counter-app
# Delete the Pod (Deployment creates a new one)
kubectl delete pod -l app=counter
# Check logs again — counter continues from where it left off!
kubectl logs -f deployment/counter-app
The counter persists because the data is stored on the PVC, not in the container filesystem.
Summary
| Storage Type | Lifetime | Use Case |
|---|---|---|
| Container filesystem | Container restart | Temp/cache data |
emptyDir |
Pod lifetime | Sharing between containers |
hostPath |
Node lifetime | Dev/testing only |
| PV + PVC (static) | Until PV deleted | Pre-provisioned storage |
| PV + PVC (dynamic) | Until PVC deleted | Cloud-native storage |
- PVCs request storage; PVs provide it
- StorageClasses enable automatic provisioning
- Access modes control how many nodes can mount a volume
- Reclaim policies control what happens when storage is released
What’s Next?
Now that you understand storage, let’s learn about StatefulSets — the controller designed for stateful applications like databases that need stable network identities and persistent storage.