CloudNativePG: production PostgreSQL in Kubernetes

Published: 2026-03-23

Running PostgreSQL in Kubernetes used to mean StatefulSets, manual failover, and custom backup scripts. CloudNativePG (CNPG) is a Kubernetes operator that handles streaming replication, automatic failover, WAL archival, scheduled backups, and point-in-time recovery declaratively. One CRD to define a cluster.


Install the operator

yamlapiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: cloudnative-pg
  namespace: cnpg-system
spec:
  chart:
    spec:
      chart: cloudnative-pg
      version: "0.x"
      sourceRef:
        kind: HelmRepository
        name: cnpg
        namespace: flux-system
  values:
    monitoring:
      podMonitorEnabled: true
      grafanaDashboard:
        create: true
        namespace: monitoring

Define a cluster

yamlapiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-main
  namespace: app
spec:
  instances: 3
  imageName: ghcr.io/cloudnative-pg/postgresql:16

  primaryUpdateStrategy: unsupervised

  storage:
    size: 20Gi
    storageClass: local-path

  resources:
    requests:
      cpu: 500m
      memory: 1Gi
    limits:
      cpu: 2000m
      memory: 2Gi

  postgresql:
    parameters:
      max_connections: "200"
      shared_buffers: "256MB"
      effective_cache_size: "768MB"
      work_mem: "4MB"
      maintenance_work_mem: "64MB"
      wal_level: logical
    pg_hba:
      - host all all 10.0.0.0/8 scram-sha-256

  bootstrap:
    initdb:
      database: myapp
      owner: myapp
      secret:
        name: pg-main-app-secret

  backup:
    retentionPolicy: "30d"
    barmanObjectStore:
      destinationPath: s3://pg-backups/pg-main
      endpointURL: https://storage.yandexcloud.net
      s3Credentials:
        accessKeyId:
          name: pg-s3-secret
          key: ACCESS_KEY_ID
        secretAccessKey:
          name: pg-s3-secret
          key: SECRET_ACCESS_KEY
      wal:
        compression: gzip
      data:
        compression: gzip

  monitoring:
    enablePodMonitor: true

instances: 3 = 1 primary + 2 replicas. CNPG manages streaming replication automatically. If the primary fails, CNPG promotes the most up-to-date replica within ~30 seconds.


Scheduled backups

yamlapiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: pg-main-daily
  namespace: app
spec:
  schedule: "0 2 * * *"
  cluster:
    name: pg-main
  backupOwnerReference: self

CNPG streams WAL to S3 continuously. The daily backup is the base backup. Together they enable point-in-time recovery to any second within the retention window.


Point-in-time recovery

yamlapiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-main-restored
  namespace: app
spec:
  instances: 1
  bootstrap:
    recovery:
      source: pg-main
      recoveryTarget:
        targetTime: "2026-10-17 14:30:00"
  externalClusters:
    - name: pg-main
      barmanObjectStore:
        destinationPath: s3://pg-backups/pg-main
        endpointURL: https://storage.yandexcloud.net
        s3Credentials:
          accessKeyId:
            name: pg-s3-secret
            key: ACCESS_KEY_ID
          secretAccessKey:
            name: pg-s3-secret
            key: SECRET_ACCESS_KEY

Use a different cluster name to avoid conflicting with production. CNPG replays WAL up to targetTime.


PgBouncer connection pooling

yamlapiVersion: postgresql.cnpg.io/v1
kind: Pooler
metadata:
  name: pg-main-pooler
  namespace: app
spec:
  cluster:
    name: pg-main
  instances: 2
  type: rw
  pgbouncer:
    poolMode: transaction
    parameters:
      max_client_conn: "1000"
      default_pool_size: "25"

type: rw routes to the primary. Use type: ro for a read-only pooler routing to replicas. Transaction pooling mode allows many clients with few backend connections.


Connecting from an application

CNPG creates a Service for each cluster:

  • pg-main-rw — primary (read-write)
  • pg-main-ro — replicas (read-only)
  • pg-main-r — all instances

Application connection string:

postgresql://myapp:password@pg-main-rw.app.svc.cluster.local:5432/myapp

With PgBouncer:

postgresql://myapp:password@pg-main-pooler.app.svc.cluster.local:5432/myapp

Useful kubectl commands

bash# Cluster status
kubectl cnpg status pg-main -n app

# List backups
kubectl get backup -n app

# Manual base backup now
kubectl cnpg backup pg-main -n app

# Promote a standby (for testing failover)
kubectl cnpg promote pg-main pg-main-2 -n app

# Check WAL archiving
kubectl cnpg status pg-main -n app | grep -A5 "WAL Archiving"

# Hibernate cluster (stop all pods, keep PVCs)
kubectl cnpg hibernate pg-main -n app

Troubleshooting

WAL archiving failing: Check S3 credentials and bucket permissions. Run kubectl cnpg status pg-main -n app to see WAL archiving errors.

Replica lagging: Check pg_replication_slots on the primary. Stuck slots block WAL cleanup and can fill the disk. Drop them with pg_drop_replication_slot if stale.

Failover didn't happen: CNPG needs quorum. With 3 instances, the primary is considered failed if at least 2 replicas report it gone. Check Pod health and node connectivity.