Databases in Containers: StatefulSets, Persistent Volumes, and Backup
Databases in Containers: StatefulSets, Persistent Volumes, and Backup
Running databases in containers was once considered an anti-pattern, but modern orchestration and storage primitives have made it viable for many production workloads. This article covers Kubernetes patterns for stateful databases, storage management, and operational considerations.
StatefulSets vs Deployments
Kubernetes Deployments are designed for stateless applications. StatefulSets are the correct primitive for databases:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: pg-secret
key: password
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "fast-ssd"
resources:
requests:
storage: 100Gi
StatefulSets provide:
* Stable, unique network identities (`pod-name-0`, `pod-name-1`).
* Ordered, graceful deployment and scaling.
* Stable storage with PersistentVolumeClaim templates.
PersistentVolumes and Storage Classes
Database storage requires careful configuration of PersistentVolumes:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: ebs.csi.aws.com
parameters:
type: gp3
iops: "3000"
throughput: "125"
reclaimPolicy: Retain
Access Modes
| Mode | Description | Database Use Case | |------|-------------|-------------------| | ReadWriteOnce | Single node read-write | Primary database | | ReadOnlyMany | Many nodes read-only | Read replicas | | ReadWriteMany | Many nodes read-write | Shared storage (avoid for PostgreSQL) |
CloudNativePG Operator
The CloudNativePG operator is the most mature Kubernetes operator for PostgreSQL:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: myapp-db
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:16
storage:
size: 100Gi
storageClass: fast-ssd
backup:
barmanObjectStore:
destinationPath: s3://myapp-backups/
s3Credentials:
accessKeyId:
name: aws-creds
key: access-key
secretAccessKey:
name: aws-creds
key: secret-key
wal:
compression: gzip
retentionPolicy: "30d"
monitoring:
enablePodMonitor: true
resources:
requests:
memory: "4Gi"
cpu: "2"
limits:
memory: "8Gi"
cpu: "4"
Automated Failover
# Simulate pod failure to test failover
kubectl delete pod myapp-db-0
# Operator automatically:
# 1. Detects primary is gone
# 2. Promotes the most advanced replica
# 3. Updates the service to point to new primary
# Typical failover time: 15-30 seconds
Backup in Containers
Volume Snapshots
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: postgres-weekly-snapshot
spec:
volumeSnapshotClassName: csi-aws-vsc
source:
persistentVolumeClaimName: data-myapp-db-0
WAL Archiving to S3
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: myapp-db-backup
spec:
schedule: "0 0 * * *"
backupOwnerReference: self
cluster:
name: myapp-db
Performance Considerations
Network Overhead
Container networking adds latency vs bare metal:
# Use hostNetwork for lowest latency
spec:
template:
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
Resource Limits
Setting proper resource limits prevents CPU throttling:
resources:
requests:
cpu: "4"
memory: "16Gi"
limits:
cpu: "8" # PostgreSQL bursts here normally
memory: "24Gi" # Allocate extra for OS cache (effective_cache_size)
Tuning for Kubernetes
# postgresql.conf tuned for container environments
shared_buffers = '4GB' # 25% of container memory limit
effective_cache_size = '12GB' # 75% of container memory limit
work_mem = '64MB'
maintenance_work_mem = '1GB'
wal_buffers = '64MB'
random_page_cost = 1.1 # SSD in containers
Anti-Patterns to Avoid
EmptyDir for Data
# WRONG: data lost on pod restart
volumes:
- name: data
emptyDir: {}
Always use PersistentVolumeClaims.
Multiple Writable Replicas
Without proper clustering (Patroni, Stolon, CNPG), multiple replicas with the same PVC cause data corruption.
Using Deployments
# WRONG: Deployments do not guarantee stable identity or storage
apiVersion: apps/v1
kind: Deployment
Always use StatefulSets for databases.
Monitoring
# Check pod status
kubectl get pods -l app=postgres
# Check PVC status
kubectl get pvc -l app=postgres
# Check WAL archiving status
kubectl exec myapp-db-1 -- psql -c "SELECT * FROM pg_stat_archiver;"
# View operator logs
kubectl logs -n postgres-operator deployment/postgres-operator
When to Containerize
**Containerize your database when**:
* You already run everything else in Kubernetes.
* You need environment parity and GitOps workflows.
* You value automated operations provided by operators.
* Your team has Kubernetes expertise.
**Do not containerize when**:
* You lack operational Kubernetes expertise.
* Your database requires specialized hardware or kernel tuning.
* You need the simplicity of managed services (RDS, Cloud SQL).
* Your team prefers a dedicated DBA toolset.
Databases in containers are production-viable with the right operator, storage class, and backup strategy. For most teams, a managed database service is simpler and more cost-effective. Containerization makes sense when you need portability, GitOps-driven management, and full control over the database configuration.