Storage in Kubernetes
Containers are ephemeral by nature — when a Pod is deleted or restarted, any data stored inside the container is lost. Kubernetes provides a storage abstraction layer that allows data to persist beyond the lifecycle of individual Pods through Volumes, PersistentVolumes (PV), and PersistentVolumeClaims (PVC).
Storage Concepts
- Volume: A directory accessible to containers in a Pod. Its lifetime is tied to the Pod.
- PersistentVolume (PV): A piece of storage provisioned by an administrator or dynamically provisioned. Exists independently of any Pod.
- PersistentVolumeClaim (PVC): A request for storage by a user. Pods use PVCs to consume PV resources.
- StorageClass: Defines the "class" of storage (SSD, HDD, NFS) and how volumes are dynamically provisioned.
PersistentVolume and PersistentVolumeClaim
# persistent-volume.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgres-pv
labels:
type: local
app: postgres
spec:
storageClassName: standard
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce # Can be mounted by a single node
persistentVolumeReclaimPolicy: Retain # Keep data after PVC is deleted
hostPath:
path: /data/postgres # Local path (for development only)
---
# persistent-volume-claim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: production
spec:
storageClassName: standard
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
selector:
matchLabels:
app: postgres
Access Modes
| Mode | Abbreviation | Description |
|---|---|---|
| ReadWriteOnce | RWO | Volume can be mounted as read-write by a single node |
| ReadOnlyMany | ROX | Volume can be mounted as read-only by many nodes |
| ReadWriteMany | RWX | Volume can be mounted as read-write by many nodes |
| ReadWriteOncePod | RWOP | Volume can be mounted as read-write by a single Pod |
StorageClasses
# storage-classes.yaml
# Fast SSD storage class (AWS)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "3000"
throughput: "125"
encrypted: "true"
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
---
# Standard storage class (GCP)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard-gcp
provisioner: pd.csi.storage.gke.io
parameters:
type: pd-standard
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
Using PVCs in Pods
# postgres-with-pvc.yaml
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_DB
value: myapp
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-credentials
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-credentials
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
StatefulSets
For stateful applications (databases, message queues), use StatefulSets instead of Deployments. StatefulSets provide stable network identities, stable persistent storage, and ordered deployment and scaling.
# statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres-headless
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "512Mi"
cpu: "500m"
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 20Gi
# Manage PVs and PVCs
kubectl get pv
kubectl get pvc -n production
kubectl describe pvc postgres-pvc -n production
# Check StorageClasses
kubectl get storageclass
# Expand a PVC (if StorageClass allows)
kubectl patch pvc postgres-pvc -n production -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
# Check StatefulSet pods (they get stable names: postgres-0, postgres-1, postgres-2)
kubectl get pods -l app=postgres
Key Takeaways
- PersistentVolumes provide storage that outlives individual Pods
- PersistentVolumeClaims are requests for storage that bind to available PVs
- StorageClasses enable dynamic provisioning — no need to pre-create PVs
- StatefulSets provide stable identities and per-replica storage for stateful apps
- Always set reclaimPolicy to Retain for production databases to prevent data loss