Volumes and Persistent Storage

5 minute read

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.

Next: StatefulSets →

Updated: