Kubernetes StatefulSet for Databases

Type: Software Reference Confidence: 0.93 Sources: 7 Verified: 2026-02-27 Freshness: 2026-02-27

TL;DR

Constraints

Quick Reference

StatefulSet vs Deployment for Databases

FeatureStatefulSetDeployment
Pod namingPredictable: {name}-0, {name}-1, ...Random: {name}-{hash}
Network identityStable DNS via headless ServiceEphemeral, load-balanced
StoragePer-Pod PVC via volumeClaimTemplatesShared or no persistent storage
Scaling orderSequential (0, 1, 2...)Parallel
Deletion orderReverse sequential (2, 1, 0)Parallel
Rolling updateReverse ordinal (highest first)Configurable (maxSurge)
Use for databasesYes -- primary/replica, stable identityOnly stateless caches
Headless Service requiredYesNo
PVC cleanup on deleteManualN/A

Database Operator Comparison

OperatorDatabaseLicenseHAAuto-BackupMonitoringCNCF
CloudNativePGPostgreSQLApache 2.0Streaming replication + auto-failoverObject store (S3, GCS, Azure) + PITRPrometheus exporterSandbox
Percona Operator PGPostgreSQLApache 2.0Patroni-based HAS3/GCS/Azure + PITRPMM integrationNo
Percona Operator MySQLMySQL (PXC)Apache 2.0Galera multi-primaryS3/GCS + PITRPMM integrationNo
Percona Operator MongoDBMongoDBApache 2.0Replica set auto-failoverS3/GCS + PITRPMM integrationNo
MongoDB CommunityMongoDBApache 2.0Replica setManualBasicNo
Zalando PG OperatorPostgreSQLMITPatroni HAWAL-G to S3/GCSBuilt-inNo
Crunchy PGOPostgreSQLApache 2.0Patroni HApgBackRestPrometheusNo

StatefulSet Pod DNS Pattern

ComponentDNS FormatExample
Pod{pod}.{service}.{namespace}.svc.cluster.localpostgres-0.postgres-hl.default.svc.cluster.local
Service (headless){service}.{namespace}.svc.cluster.localpostgres-hl.default.svc.cluster.local
Primary (convention){statefulset}-0.{service}.{ns}.svc.cluster.localpostgres-0.postgres-hl.default.svc.cluster.local

Decision Tree

START: Do you need a database on Kubernetes?
├── Can you use a managed database (RDS, Cloud SQL, Azure DB)?
│   ├── YES → Use managed database. Simplest, most reliable option.
│   └── NO (on-prem, air-gapped, cost, or data sovereignty) ↓
├── Team has 3+ engineers with Kubernetes experience?
│   ├── YES → Use a database operator (CloudNativePG, Percona, Crunchy PGO)
│   └── NO ↓
├── Database is PostgreSQL?
│   ├── YES → CloudNativePG (simplest operator, CNCF, strong community)
│   └── NO ↓
├── Database is MySQL?
│   ├── YES → Percona Operator for MySQL (Galera-based HA)
│   └── NO ↓
├── Database is MongoDB?
│   ├── YES → Percona Operator for MongoDB (replica set + sharding)
│   └── NO ↓
├── Need fine-grained control or learning exercise?
│   ├── YES → Manual StatefulSet (see Step-by-Step Guide)
│   └── NO ↓
└── DEFAULT → CloudNativePG for PostgreSQL, Percona for MySQL/MongoDB.

Step-by-Step Guide

1. Create a headless Service

The headless Service provides stable DNS names for each Pod. Without it, StatefulSet Pods cannot be individually addressed. [src1]

apiVersion: v1
kind: Service
metadata:
  name: postgres-hl
spec:
  clusterIP: None          # Headless
  selector:
    app: postgres
  ports:
    - port: 5432
      name: postgres

Verify: kubectl get svc postgres-hl → should show CLUSTER-IP: None

2. Create a Secret for database credentials

Never store passwords in plain text in StatefulSet YAML. Use Kubernetes Secrets. [src5]

apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
type: Opaque
stringData:
  POSTGRES_PASSWORD: "changeme-use-strong-password"
  POSTGRES_USER: "postgres"
  POSTGRES_DB: "appdb"

Verify: kubectl get secret postgres-secretOpaque type with 3 data keys

3. Deploy the StatefulSet with volumeClaimTemplates

Creates a PostgreSQL instance with persistent storage. Each replica gets its own PVC. [src1] [src5]

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres-hl
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16-alpine
          # ... (see full YAML in Code Examples)
  volumeClaimTemplates:
    - metadata:
        name: postgres-data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: standard
        resources:
          requests:
            storage: 10Gi

Verify: kubectl get statefulset postgresREADY: 1/1; kubectl get pvcpostgres-data-postgres-0 Bound

4. Configure init containers for replica bootstrap

For primary/replica setups, use init containers to determine the Pod's role based on its ordinal index. [src2]

initContainers:
  - name: init-role
    image: postgres:16-alpine
    command: ['sh', '-c']
    args:
      - |
        ORDINAL=$(echo $HOSTNAME | rev | cut -d'-' -f1 | rev)
        if [ "$ORDINAL" = "0" ]; then
          echo "primary" > /config/role
        else
          echo "replica" > /config/role
        fi

Verify: kubectl exec postgres-0 -- cat /config/roleprimary

5. Set up a backup CronJob

Use the database's native backup tool in a CronJob, not filesystem snapshots. [src7]

apiVersion: batch/v1
kind: CronJob
metadata:
  name: postgres-backup
spec:
  schedule: "0 2 * * *"   # Daily at 2 AM
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: backup
              image: postgres:16-alpine
              command: ['sh', '-c']
              args:
                - pg_dump -h postgres-0.postgres-hl -U postgres -Fc appdb > /backup/$(date +%Y%m%d).dump

Verify: kubectl get cronjob postgres-backup → schedule 0 2 * * *

6. Deploy with a database operator (recommended for production)

For production PostgreSQL, install CloudNativePG and declare a Cluster resource. [src3]

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: app-db
spec:
  instances: 3
  storage:
    size: 20Gi
    storageClass: standard
  backup:
    barmanObjectStore:
      destinationPath: s3://my-bucket/backups

Verify: kubectl get cluster app-dbPhase: Cluster in healthy state

Code Examples

YAML: Complete PostgreSQL StatefulSet

Full script: postgres-statefulset.yaml (48 lines)

# Input:  Kubernetes cluster with dynamic storage provisioner
# Output: Single PostgreSQL instance with persistent storage

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres-hl
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16-alpine
          ports:
            - containerPort: 5432
          envFrom:
            - secretRef:
                name: postgres-secret
          env:
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          volumeMounts:
            - name: postgres-data
              mountPath: /var/lib/postgresql/data
          resources:
            requests:
              cpu: 250m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 2Gi
          readinessProbe:
            exec:
              command: ["pg_isready", "-U", "postgres"]
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            exec:
              command: ["pg_isready", "-U", "postgres"]
            initialDelaySeconds: 30
            periodSeconds: 15
  volumeClaimTemplates:
    - metadata:
        name: postgres-data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: standard
        resources:
          requests:
            storage: 10Gi

YAML: CloudNativePG Cluster with Automated Backups

# Input:  Kubernetes cluster with CloudNativePG operator installed
# Output: 3-node PostgreSQL HA cluster with S3 backups + PITR

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: production-db
spec:
  instances: 3
  postgresql:
    parameters:
      max_connections: "200"
      shared_buffers: "512MB"
  storage:
    size: 50Gi
    storageClass: fast-ssd
  backup:
    retentionPolicy: "30d"
    barmanObjectStore:
      destinationPath: s3://backups/production-db

Anti-Patterns

Wrong: Using a Deployment for a database with replication

# BAD -- Deployment gives random Pod names, no stable DNS for replication
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: postgres
          image: postgres:16-alpine
      volumes:
        - name: data
          emptyDir: {}  # Data lost on Pod restart!

Correct: Using a StatefulSet with persistent volumes

# GOOD -- StatefulSet provides stable identity and persistent storage
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres-hl
  replicas: 3
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 10Gi

Wrong: Using a regular ClusterIP Service for StatefulSet

# BAD -- ClusterIP Service load-balances; replicas can't address each other
apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  selector:
    app: postgres
  ports:
    - port: 5432
  # Missing clusterIP: None -- creates a load-balanced Service

Correct: Using a headless Service

# GOOD -- headless Service creates individual DNS records per Pod
apiVersion: v1
kind: Service
metadata:
  name: postgres-hl
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
    - port: 5432

Wrong: Backing up a database by copying PVC files

# BAD -- copying data directory while database is running produces corrupt backup
kubectl cp postgres-0:/var/lib/postgresql/data ./backup/
# WAL files may be inconsistent; you'll get a corrupted restore

Correct: Using the database's native dump tool

# GOOD -- pg_dump creates a consistent logical backup
kubectl exec postgres-0 -- pg_dump -U postgres -Fc appdb > backup.dump

# GOOD -- for physical backup, use pg_basebackup
kubectl exec postgres-0 -- pg_basebackup -D /tmp/backup -Ft -z -P

Wrong: Hardcoding passwords in StatefulSet YAML

# BAD -- credentials visible in version control and kubectl describe
env:
  - name: POSTGRES_PASSWORD
    value: "mysecretpassword"

Correct: Using Kubernetes Secrets

# GOOD -- reference Secret for credentials
envFrom:
  - secretRef:
      name: postgres-secret

Common Pitfalls

Diagnostic Commands

# List StatefulSet status and ready replicas
kubectl get statefulset postgres -o wide

# Check PVC status (should all be Bound)
kubectl get pvc -l app=postgres

# View Pod DNS resolution from inside the cluster
kubectl run -it --rm debug --image=busybox -- nslookup postgres-0.postgres-hl

# Check database readiness from Pod
kubectl exec postgres-0 -- pg_isready -U postgres

# View StatefulSet events (useful for debugging stuck rollouts)
kubectl describe statefulset postgres

# Check storage class availability
kubectl get storageclass

# View Pod logs for database startup errors
kubectl logs postgres-0 --tail=50

# Check PV reclaim policy (should be Retain for production)
kubectl get pv -o custom-columns=NAME:.metadata.name,RECLAIM:.spec.persistentVolumeReclaimPolicy,STATUS:.status.phase

# Check operator status (CloudNativePG)
kubectl get cluster -A
kubectl get pods -n cnpg-system

Version History & Compatibility

Kubernetes StatefulSet Features

K8s VersionFeatureStatusNotes
1.9+StatefulSet APIGA (apps/v1)Stable since 2017
1.24+PVC auto-deletionBetaStatefulSetAutoDeletePVC feature gate
1.25+minReadySecondsGAPod must be ready for N seconds
1.26+Start ordinalBetaCustom starting ordinal index
1.27+PVC resize for StatefulSetsStableExpand PVCs without recreation
1.31+Start ordinalGACustom ordinal ranges fully stable
1.31+maxUnavailable for RollingUpdateBetaParallel rolling updates

Database Operators

OperatorLatest VersionMin K8sDatabase Support
CloudNativePG1.25.x1.27+PostgreSQL 12-17
Percona Operator PG2.5.x1.27+PostgreSQL 13-17
Percona Operator MySQL1.16.x1.26+MySQL 8.0 (PXC)
Percona Operator MongoDB1.18.x1.26+MongoDB 6.0-8.0

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Database requires stable Pod identity for primary/replica topologyApplication is stateless (web servers, API gateways)Deployment
Need ordered startup (primary before replicas)Database can tolerate random Pod namesDeployment with PVC
Per-Pod persistent storage is requiredAll replicas share the same data volumeDeployment + single PVC
Running on-prem or air-gapped (no managed DB option)Cloud provider offers managed databaseManaged database service
Dev/test environment needing realistic database setupProduction DB needing automated failover and PITRDatabase operator (CloudNativePG, Percona)
Learning Kubernetes stateful workloadsTeam lacks Kubernetes operational expertiseManaged database service

Important Caveats

Related Units