PV & PVC Patterns
Understanding common patterns for using PersistentVolumes and PersistentVolumeClaims helps you design storage solutions that work well for your applications. This guide covers the most common patterns, from simple single-pod storage to complex multi-pod shared storage scenarios.
Static vs Dynamic Provisioning
The first decision is whether to use static or dynamic provisioning.
Static Provisioning Pattern
With static provisioning, administrators manually create PVs, and users create PVCs that bind to these pre-created volumes.
Example:
# Admin creates PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-static
spec:
capacity:
storage: 50Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: manual
hostPath:
path: /mnt/data
---
# User creates PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-static
spec:
accessModes:
- ReadWriteOnce
storageClassName: manual
resources:
requests:
storage: 50Gi
---
# Pod uses PVC
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: nginx
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: pvc-static
When to use:
- Pre-existing storage that needs to be mapped
- Specific storage requirements not supported by dynamic provisioning
- Legacy storage systems
- Testing and development
Dynamic Provisioning Pattern
With dynamic provisioning, users create PVCs that reference a StorageClass, and Kubernetes automatically creates the PV.
Example:
# StorageClass (typically created by admin)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard
provisioner: pd.csi.storage.gke.io
parameters:
type: pd-standard
reclaimPolicy: Delete
---
# User creates PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-dynamic
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
resources:
requests:
storage: 50Gi
---
# Pod uses PVC
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
containers:
- name: app
image: nginx
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: pvc-dynamic
When to use:
- Most production use cases (recommended)
- When you want automatic volume creation
- Cloud-native applications
- Scalable storage requirements
Single Pod Pattern
The simplest pattern: one pod with one PVC.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: single-pod-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: single-pod-app
spec:
replicas: 1
template:
spec:
containers:
- name: app
image: nginx
volumeMounts:
- name: data
mountPath: /var/www/html
volumes:
- name: data
persistentVolumeClaim:
claimName: single-pod-pvc
Characteristics:
- One PVC per pod
- ReadWriteOnce access mode
- Simple and straightforward
- Good for single-instance applications
StatefulSet Pattern
StatefulSets provide stable pod identities and ordered deployment. Each pod gets its own PVC through volumeClaimTemplates.
Example:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: database
spec:
serviceName: database
replicas: 3
template:
metadata:
labels:
app: database
spec:
containers:
- name: db
image: postgres:14
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates: # Creates PVC for each pod
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd
resources:
requests:
storage: 100Gi
This creates:
data-database-0PVC for poddatabase-0data-database-1PVC for poddatabase-1data-database-2PVC for poddatabase-2
Characteristics:
- Each pod gets its own PVC
- Stable pod names and PVC names
- Ordered deployment and scaling
- Perfect for databases and stateful applications
Shared Storage Pattern
Multiple pods sharing the same storage using ReadWriteMany access mode.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: shared-content
spec:
accessModes:
- ReadWriteMany # Multiple pods can mount
storageClassName: nfs-storage
resources:
requests:
storage: 500Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
spec:
replicas: 5
template:
spec:
containers:
- name: nginx
image: nginx:latest
volumeMounts:
- name: content
mountPath: /usr/share/nginx/html
volumes:
- name: content
persistentVolumeClaim:
claimName: shared-content
Characteristics:
- Single PVC shared by multiple pods
- Requires ReadWriteMany access mode
- Needs network-based storage (NFS, cloud file storage)
- Good for shared content, web servers, CMS
Multiple Volumes Pattern
A single pod using multiple PVCs for different purposes.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd
resources:
requests:
storage: 100Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-logs
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
resources:
requests:
storage: 50Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 1
template:
spec:
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: data
mountPath: /var/app/data
- name: logs
mountPath: /var/app/logs
volumes:
- name: data
persistentVolumeClaim:
claimName: app-data
- name: logs
persistentVolumeClaim:
claimName: app-logs
Characteristics:
- Different storage classes for different needs
- Separation of concerns (data vs logs)
- Different performance tiers
- Different lifecycle management
Init Container Pattern
Using init containers to prepare or populate volumes before the main container starts.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 1
template:
spec:
initContainers:
- name: init-data
image: busybox
command: ["/bin/sh", "-c"]
args:
- |
echo "Initializing data..."
cp /source/* /data/
volumeMounts:
- name: data
mountPath: /data
- name: source
mountPath: /source
readOnly: true
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: data
mountPath: /var/app/data
volumes:
- name: data
persistentVolumeClaim:
claimName: app-data
- name: source
configMap:
name: initial-data
Characteristics:
- Init containers run before main containers
- Can populate or prepare volumes
- Useful for data migration or initialization
- Ensures data is ready before app starts
Common Pitfalls and Solutions
Pitfall 1: PVC Stuck in Pending
Problem: PVC remains in Pending state.
Causes:
- No StorageClass matches
- StorageClass doesn’t exist
- Provisioner not available
- Insufficient storage quota
Solution:
# Check StorageClass exists
kubectl get storageclass
# Check PVC events
kubectl describe pvc <pvc-name>
# Verify provisioner is running
kubectl get pods -n kube-system | grep provisioner
Pitfall 2: Multiple Pods Can’t Share RWO Volume
Problem: Trying to mount a ReadWriteOnce PVC in multiple pods.
Solution: Use ReadWriteMany access mode (requires NFS or compatible storage):
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: shared-pvc
spec:
accessModes:
- ReadWriteMany # Changed from ReadWriteOnce
storageClassName: nfs-storage
resources:
requests:
storage: 100Gi
Or use StatefulSet with volumeClaimTemplates (each pod gets its own PVC).
Pitfall 3: Data Lost on Pod Restart
Problem: Data disappears when pod restarts.
Causes:
- Using ephemeral volumes (emptyDir) instead of PVC
- PVC not properly mounted
Solution: Ensure you’re using a PVC, not an ephemeral volume:
volumes:
- name: data
persistentVolumeClaim: # Correct
claimName: my-pvc
# emptyDir: {} # Wrong - this is ephemeral
Pitfall 4: Volume in Wrong Zone
Problem: Volume created in different zone than pod, causing performance issues.
Solution: Use WaitForFirstConsumer binding mode:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: zonal-storage
provisioner: pd.csi.storage.gke.io
volumeBindingMode: WaitForFirstConsumer # Wait for pod scheduling
Best Practices
- Use dynamic provisioning - Prefer StorageClasses over static provisioning
- Use StatefulSets for stateful apps - Provides stable identities and ordered deployment
- Match access mode to use case - RWO for single pod, RWX for shared storage
- Set appropriate sizes - Don’t over-provision, but leave room for growth
- Use volumeClaimTemplates - For StatefulSets, use templates instead of manual PVCs
- Separate concerns - Use different PVCs for different data types (data vs logs)
- Test cleanup procedures - Understand what happens when PVCs are deleted
- Monitor storage usage - Track PVC sizes and actual usage
See Also
- PVs & PVCs - Overview of PersistentVolumes and PersistentVolumeClaims
- StatefulSets - Managing stateful applications
- StorageClasses - Dynamic provisioning
- Access Modes - Understanding access modes