---
# === IDENTITY ===
id: software/devops/k8s-statefulset-databases/2026
canonical_question: "Kubernetes reference: StatefulSet for databases"
aliases:
  - "Kubernetes StatefulSet database deployment"
  - "how to run a database on Kubernetes"
  - "StatefulSet vs Deployment for databases"
  - "Kubernetes persistent volume database"
  - "k8s StatefulSet PostgreSQL MySQL"
  - "database operator Kubernetes"
  - "headless service StatefulSet"
  - "volumeClaimTemplates database"
entity_type: software_reference
domain: software > devops > Kubernetes StatefulSet for Databases
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-27
confidence: 0.93
version: 1.0
first_published: 2026-02-27

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "Kubernetes 1.27: StatefulSet start ordinals (2023-04); Kubernetes 1.31: configurable ordinal ranges GA (2024-08)"
  next_review: 2026-08-26
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "StatefulSets REQUIRE a headless Service (clusterIP: None) -- without it, Pods have no stable DNS names for replication"
  - "volumeClaimTemplates CANNOT be modified after StatefulSet creation -- plan storage size and class before deploying"
  - "PersistentVolumeClaims are NOT automatically deleted when StatefulSet is scaled down or deleted -- orphaned PVCs accumulate cost"
  - "NEVER use Deployment for databases that require stable network identity or ordered startup (e.g., primary/replica topology)"
  - "Database backups MUST use the database's native dump tool (pg_dump, mysqldump, mongodump) -- filesystem-level PVC snapshots can produce corrupt backups"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Running a stateless web application or microservice without persistent data"
    use_instead: "software/devops/k8s-deployment-service-ingress/2026"
  - condition: "Need to autoscale application Pods based on CPU/memory"
    use_instead: "software/devops/k8s-hpa/2026"
  - condition: "Using a managed database service (RDS, Cloud SQL, Azure Database)"
    use_instead: "Use your cloud provider's managed database documentation"

# === AGENT HINTS ===
inputs_needed:
  - key: "database_engine"
    question: "Which database engine are you deploying?"
    type: choice
    options: ["PostgreSQL", "MySQL", "MongoDB", "Redis", "Cassandra", "Other"]
  - key: "management_approach"
    question: "Do you want to manage the database manually or use an operator?"
    type: choice
    options: ["Manual StatefulSet", "Kubernetes Operator", "Unsure"]
  - key: "team_size"
    question: "How large is your platform/DevOps team?"
    type: choice
    options: ["1-2 engineers", "3-5 engineers", "6+ engineers"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/devops/k8s-statefulset-databases/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-27)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "software/devops/k8s-deployment-service-ingress/2026"
      label: "Kubernetes Deployment, Service & Ingress Basics"
  related_to:
    - id: "software/devops/k8s-hpa/2026"
      label: "Kubernetes Horizontal Pod Autoscaler"
  solves: []
  alternative_to: []
  often_confused_with: []

# === SOURCES (7 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "StatefulSets"
    author: Kubernetes
    url: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/
    type: official_docs
    published: 2025-12-01
    reliability: authoritative
  - id: src2
    title: "Run a Replicated Stateful Application"
    author: Kubernetes
    url: https://kubernetes.io/docs/tasks/run-application/run-replicated-stateful-application/
    type: official_docs
    published: 2025-12-01
    reliability: authoritative
  - id: src3
    title: "CloudNativePG - PostgreSQL Operator for Kubernetes"
    author: CloudNativePG
    url: https://cloudnative-pg.io/
    type: official_docs
    published: 2025-10-01
    reliability: high
  - id: src4
    title: "Kubernetes Operators Compared: The Key to Scalable, Cost-Efficient Databases"
    author: Percona
    url: https://www.percona.com/blog/kubernetes-operators-compared
    type: technical_blog
    published: 2025-06-15
    reliability: high
  - id: src5
    title: "How to Deploy PostgreSQL Statefulset Cluster on Kubernetes"
    author: DevOpsCube
    url: https://devopscube.com/deploy-postgresql-statefulset/
    type: technical_blog
    published: 2025-03-10
    reliability: moderate_high
  - id: src6
    title: "How to choose your Kubernetes Postgres Operator?"
    author: Simplyblock
    url: https://www.simplyblock.io/blog/choosing-a-kubernetes-postgres-operator/
    type: technical_blog
    published: 2025-07-20
    reliability: moderate_high
  - id: src7
    title: "A Practical Guide to Kubernetes Stateful Backup and Recovery"
    author: The New Stack
    url: https://thenewstack.io/a-practical-guide-to-kubernetes-stateful-backup-and-recovery/
    type: technical_blog
    published: 2025-04-15
    reliability: moderate_high
---

# Kubernetes StatefulSet for Databases

## TL;DR

- **Bottom line**: Use StatefulSets when you need stable Pod identities and persistent storage for databases on Kubernetes; use a database operator (CloudNativePG, Percona) for production to automate failover, backups, and replication.
- **Key tool/command**: `kubectl apply -f statefulset.yaml` with `volumeClaimTemplates` and a headless Service (`clusterIP: None`)
- **Watch out for**: volumeClaimTemplates cannot be modified after creation, and PVCs are never automatically deleted on scale-down -- orphaned PVCs silently accumulate cost.
- **Works with**: Kubernetes 1.26+ (StatefulSet GA since 1.9; start ordinals GA in 1.31). All major database engines.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- StatefulSets REQUIRE a headless Service (clusterIP: None) -- without it, Pods have no stable DNS names for replication
- volumeClaimTemplates CANNOT be modified after StatefulSet creation -- plan storage size and class before deploying
- PersistentVolumeClaims are NOT automatically deleted when StatefulSet is scaled down or deleted -- orphaned PVCs accumulate cost
- NEVER use Deployment for databases that require stable network identity or ordered startup (e.g., primary/replica topology)
- Database backups MUST use the database's native dump tool (pg_dump, mysqldump, mongodump) -- filesystem-level PVC snapshots can produce corrupt backups

## Quick Reference

### StatefulSet vs Deployment for Databases

| Feature | StatefulSet | Deployment |
|---|---|---|
| Pod naming | Predictable: `{name}-0`, `{name}-1`, ... | Random: `{name}-{hash}` |
| Network identity | Stable DNS via headless Service | Ephemeral, load-balanced |
| Storage | Per-Pod PVC via volumeClaimTemplates | Shared or no persistent storage |
| Scaling order | Sequential (0, 1, 2...) | Parallel |
| Deletion order | Reverse sequential (2, 1, 0) | Parallel |
| Rolling update | Reverse ordinal (highest first) | Configurable (maxSurge) |
| Use for databases | Yes -- primary/replica, stable identity | Only stateless caches (Redis sentinel excluded) |
| Headless Service required | Yes | No |
| PVC cleanup on delete | Manual | N/A (no volumeClaimTemplates) |

### Database Operator Comparison

| Operator | Database | License | HA | Auto-Backup | Monitoring | CNCF |
|---|---|---|---|---|---|---|
| CloudNativePG | PostgreSQL | Apache 2.0 | Streaming replication + auto-failover | Object store (S3, GCS, Azure) + PITR | Prometheus exporter | Sandbox project |
| Percona Operator for PostgreSQL | PostgreSQL | Apache 2.0 | Patroni-based HA | S3/GCS/Azure + PITR | PMM integration | No |
| Percona Operator for MySQL | MySQL (PXC) | Apache 2.0 | Galera multi-primary | S3/GCS + PITR | PMM integration | No |
| Percona Operator for MongoDB | MongoDB | Apache 2.0 | Replica set auto-failover | S3/GCS + PITR | PMM integration | No |
| MongoDB Community Operator | MongoDB | Apache 2.0 | Replica set | Manual | Basic | No |
| Zalando Postgres Operator | PostgreSQL | MIT | Patroni HA | WAL-G to S3/GCS | Built-in | No |
| Crunchy PGO | PostgreSQL | Apache 2.0 | Patroni HA | pgBackRest | Prometheus | No |

### StatefulSet Pod DNS Pattern

| Component | DNS Format | Example |
|---|---|---|
| Pod | `{pod}.{service}.{namespace}.svc.cluster.local` | `postgres-0.postgres-hl.default.svc.cluster.local` |
| Service (headless) | `{service}.{namespace}.svc.cluster.local` | `postgres-hl.default.svc.cluster.local` |
| Primary (convention) | `{statefulset}-0.{service}.{namespace}.svc.cluster.local` | `postgres-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)
│   │         Operators handle failover, backups, scaling automatically.
│   └── 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 below)
│   └── NO ↓
└── DEFAULT → CloudNativePG for PostgreSQL, Percona for MySQL/MongoDB.
              Manual StatefulSet only for dev/test or unsupported engines.
```

## 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]

```yaml
# postgres-headless-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: postgres-hl
  labels:
    app: postgres
spec:
  clusterIP: None          # Headless -- no load balancer IP
  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]

```yaml
# postgres-secret.yaml
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-secret` --> should show `Opaque` type with 3 data keys

### 3. Deploy the StatefulSet with volumeClaimTemplates

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

> Full script: [postgres-statefulset.yaml](scripts/postgres-statefulset.yaml) (48 lines)

```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres-hl   # Must match headless Service name
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    # ... (see full script)
  volumeClaimTemplates:
    - metadata:
        name: postgres-data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: standard
        resources:
          requests:
            storage: 10Gi
```

**Verify**: `kubectl get statefulset postgres` --> `READY: 1/1`; `kubectl get pvc` --> should show `postgres-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]

> Full script: [postgres-replica-init.yaml](scripts/postgres-replica-init.yaml) (40 lines)

```yaml
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
    # ... (see full script)
```

**Verify**: `kubectl exec postgres-0 -- cat /config/role` --> `primary`; `kubectl exec postgres-1 -- cat /config/role` --> `replica`

### 5. Set up a backup CronJob

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

> Full script: [postgres-backup-cronjob.yaml](scripts/postgres-backup-cronjob.yaml) (30 lines)

```yaml
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
              # ... (see full script)
```

**Verify**: `kubectl get cronjob postgres-backup` --> should show schedule `0 2 * * *`

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

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

```bash
# Install CloudNativePG operator
kubectl apply --server-side -f \
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.25/releases/cnpg-1.25.0.yaml
```

```yaml
# cnpg-cluster.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: app-db
spec:
  instances: 3                    # 1 primary + 2 replicas
  storage:
    size: 20Gi
    storageClass: standard
  backup:
    barmanObjectStore:
      destinationPath: s3://my-bucket/backups
      s3Credentials:
        accessKeyId:
          name: aws-creds
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: aws-creds
          key: SECRET_ACCESS_KEY
```

**Verify**: `kubectl get cluster app-db` --> `Phase: Cluster in healthy state`; `kubectl get pods -l cnpg.io/cluster=app-db` --> 3 running Pods

## Code Examples
<!-- Keep inline examples <=15 lines. For longer scripts, extract to scripts/ subdirectory
     and link: "Full script: [name.ext](scripts/name.ext) (N lines)" -->

### YAML: Complete PostgreSQL StatefulSet

> Full script: [postgres-statefulset.yaml](scripts/postgres-statefulset.yaml) (48 lines)

```yaml
# 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: MySQL StatefulSet with Replication Init Container

> Full script: [mysql-statefulset-replication.yaml](scripts/mysql-statefulset-replication.yaml) (62 lines)

```yaml
# Input:  Kubernetes cluster with dynamic provisioner + mysql-secret
# Output: MySQL primary (ordinal 0) + read replicas

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-config
data:
  primary.cnf: |
    [mysqld]
    log-bin=mysql-bin
    server-id=1
  replica.cnf: |
    [mysqld]
    super-read-only
# ... (see full script for StatefulSet with init containers)
```

### YAML: CloudNativePG Cluster with Automated Backups

```yaml
# 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

```yaml
# 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

```yaml
# GOOD -- StatefulSet provides stable identity and persistent storage
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres-hl   # Headless Service required
  replicas: 3
  volumeClaimTemplates:      # Each Pod gets its own PVC
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 10Gi
```

### Wrong: Using a regular ClusterIP Service for StatefulSet

```yaml
# 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 -- this creates a load-balanced Service
```

### Correct: Using a headless Service

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

### Wrong: Backing up a database by copying PVC files

```bash
# 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

```bash
# 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

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

### Correct: Using Kubernetes Secrets

```yaml
# GOOD -- reference Secret for credentials
envFrom:
  - secretRef:
      name: postgres-secret
# Or individual key:
env:
  - name: POSTGRES_PASSWORD
    valueFrom:
      secretKeyRef:
        name: postgres-secret
        key: POSTGRES_PASSWORD
```

## Common Pitfalls

- **volumeClaimTemplates immutability**: You cannot resize or change StorageClass after StatefulSet creation. Fix: Create a new StatefulSet with updated templates, migrate data, then delete the old one. [src1]
- **Orphaned PVCs after scale-down**: Scaling from 3 to 1 replica leaves PVCs for Pod-1 and Pod-2 allocated and billed. Fix: Manually delete PVCs after confirming data is no longer needed: `kubectl delete pvc postgres-data-postgres-1 postgres-data-postgres-2`. [src1]
- **PGDATA subdirectory requirement**: PostgreSQL requires PGDATA to be a subdirectory, not the volume root. Fix: Set `PGDATA=/var/lib/postgresql/data/pgdata` and mount the volume at `/var/lib/postgresql/data`. [src5]
- **Missing readiness probes**: Without readiness probes, Kubernetes considers Pods ready immediately, causing replication to start before the database is initialized. Fix: Add `pg_isready` (PostgreSQL) or `mysqladmin ping` (MySQL) as readiness probe. [src2]
- **StorageClass mismatch**: Using `ReadWriteMany` when the storage provider only supports `ReadWriteOnce` causes PVC binding failures. Fix: Check `kubectl get storageclass` and match access modes. [src5]
- **Pod stuck in Pending after node failure**: If a node fails and the PV uses local storage, the Pod cannot reschedule. Fix: Use network-attached storage (EBS, Ceph, GCE PD) for production databases. [src7]
- **Ignoring Pod Disruption Budgets**: Without a PDB, cluster upgrades can take down all database Pods simultaneously. Fix: Create a PDB with `minAvailable: 1` for database StatefulSets. [src1]
- **Operator version drift**: Running an outdated operator version misses critical security patches and bug fixes. Fix: Pin operator versions and include them in your upgrade runbook. [src4]

## Diagnostic Commands

```bash
# 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

# List orphaned PVCs (not used by any Pod)
kubectl get pvc --no-headers | while read name status vol cap access sc age; do
  kubectl get pod -l app=postgres --no-headers 2>/dev/null | grep -q "${name##*-}" || echo "Possibly orphaned: $name"
done

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

## Version History & Compatibility

| Kubernetes Version | StatefulSet Feature | Status | Notes |
|---|---|---|---|
| 1.9+ | StatefulSet API | GA (apps/v1) | Stable since 2017 |
| 1.24+ | PVC auto-deletion (whenDeleted/whenScaled) | Beta | `StatefulSetAutoDeletePVC` feature gate |
| 1.25+ | minReadySeconds | GA | Pod must be ready for N seconds |
| 1.26+ | Start ordinal (ordinals.start) | Beta | Custom starting ordinal index |
| 1.27+ | PVC resize for StatefulSets | Stable | Expand PVCs without recreation |
| 1.31+ | Start ordinal | GA | Custom ordinal ranges fully stable |
| 1.31+ | maxUnavailable for RollingUpdate | Beta | Parallel rolling updates |

| Operator | Latest Version | Min K8s | Database Support |
|---|---|---|---|
| CloudNativePG | 1.25.x | 1.27+ | PostgreSQL 12-17 |
| Percona Operator PG | 2.5.x | 1.27+ | PostgreSQL 13-17 |
| Percona Operator MySQL | 1.16.x | 1.26+ | MySQL 8.0 (PXC) |
| Percona Operator MongoDB | 1.18.x | 1.26+ | MongoDB 6.0-8.0 |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Database requires stable Pod identity for primary/replica topology | Application is stateless (web servers, API gateways) | Deployment |
| Need ordered startup (primary before replicas) | Database can tolerate random Pod names | Deployment with PVC |
| Per-Pod persistent storage is required | All replicas share the same data volume | Deployment + single PVC |
| Running on-prem or air-gapped (no managed DB option) | Cloud provider offers managed database (RDS, Cloud SQL) | Managed database service |
| Dev/test environment needing realistic database setup | Production database requiring automated failover and PITR | Database operator (CloudNativePG, Percona) |
| Learning Kubernetes stateful workloads | Team lacks Kubernetes operational expertise | Managed database service |

## Important Caveats

- StatefulSet PVC auto-deletion (`persistentVolumeClaimRetentionPolicy`) is Beta as of Kubernetes 1.31 -- do not rely on it in production without testing; the default behavior is to retain PVCs
- CloudNativePG does NOT use StatefulSets internally -- it manages Pods directly for finer control over failover; understanding StatefulSets is still essential for other databases and for understanding the concepts
- volumeClaimTemplates storage size can be expanded (since K8s 1.27) only if the StorageClass has `allowVolumeExpansion: true` -- shrinking is never supported
- Running databases on Kubernetes adds operational complexity; for teams without dedicated platform engineers, managed database services (RDS, Cloud SQL, Azure Database) are usually more cost-effective when accounting for engineering time
- Pod Disruption Budgets (PDB) are critical for database StatefulSets -- without them, cluster upgrades may simultaneously evict all database Pods, causing data loss
- StatefulSet rolling updates proceed in reverse ordinal order (highest to lowest), which means replicas update before the primary -- verify your replication topology handles this correctly

## Related Units
<!-- Generated from related_kos frontmatter -->

- [Kubernetes Deployment, Service & Ingress Basics](/software/devops/k8s-deployment-service-ingress/2026)
- [Kubernetes Horizontal Pod Autoscaler](/software/devops/k8s-hpa/2026)
