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.

graph LR A[Admin Creates PV] --> B[User Creates PVC] B --> C[Kubernetes Binds] C --> D[Pod Uses PVC] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#e8f5e9 style D fill:#f3e5f5

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.

graph LR A[User Creates PVC with StorageClass] --> B[Kubernetes Provisions PV] B --> C[PV Bound to PVC] C --> D[Pod Uses PVC] style A fill:#e1f5ff style B fill:#e8f5e9 style C fill:#fff4e1 style D fill:#f3e5f5

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.

graph TB A[StatefulSet] --> B[Pod 0] A --> C[Pod 1] A --> D[Pod 2] B --> E[PVC: data-0] C --> F[PVC: data-1] D --> G[PVC: data-2] style A fill:#e1f5ff style E fill:#fff4e1 style F fill:#fff4e1 style G fill:#fff4e1

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-0 PVC for pod database-0
  • data-database-1 PVC for pod database-1
  • data-database-2 PVC for pod database-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

  1. Use dynamic provisioning - Prefer StorageClasses over static provisioning
  2. Use StatefulSets for stateful apps - Provides stable identities and ordered deployment
  3. Match access mode to use case - RWO for single pod, RWX for shared storage
  4. Set appropriate sizes - Don’t over-provision, but leave room for growth
  5. Use volumeClaimTemplates - For StatefulSets, use templates instead of manual PVCs
  6. Separate concerns - Use different PVCs for different data types (data vs logs)
  7. Test cleanup procedures - Understand what happens when PVCs are deleted
  8. Monitor storage usage - Track PVC sizes and actual usage

See Also