JusDB
Free DB Audit
PostgreSQL

CloudNativePG: Running PostgreSQL on Kubernetes in Production

Run PostgreSQL on Kubernetes with CloudNativePG 1.30: a production Cluster manifest, synchronous replication, failover, Barman Cloud Plugin backups, PITR, PgBouncer, and upgrades.

JusDB Team
Published November 3, 2026
18 min read

CloudNativePG 1.30.1, released on 23 September 2026, is the current release of the PostgreSQL operator that the CNCF moved to Incubating status on 2 November 2026. Production teams tend to hit the same problems: two instances on one node because anti-affinity was left at its soft default, a primary whose pg_wal fills because WAL archiving stopped, and backups still configured through .spec.backup.barmanObjectStore, the in-tree Barman Cloud integration that has been deprecated since 1.26 and is removed starting from 1.32.0. This guide covers the production Cluster manifest, replication, failover, backups, PITR, pooling, upgrades, and monitoring, with the kubectl cnpg command for each.

TL;DR
  • Run CloudNativePG 1.30.x (1.30.1 as of November 2026). 1.29 reached end of life on 29 September 2026, and 1.31 is planned for around December 2026.
  • Back up new clusters with the Barman Cloud Plugin (CNPG-I) and a minimal or standard image. Native barmanObjectStore support is removed starting from 1.32.0, and the system images go with it.
  • On three instances, set synchronous to method: any and number: 1, then choose dataDurability: required (writes pause without a synchronous standby) or preferred (writes continue).
  • Major upgrades are declarative since 1.26: change the image's major version and the operator runs pg_upgrade --link offline. The whole cluster is down, replicas are re-cloned, and older backups cannot be used for PITR past the upgrade.

Scope and review date: November 2026. This post covers CloudNativePG 1.30.1 with the Barman Cloud Plugin 0.15.1, on Kubernetes 1.34 to 1.36 and PostgreSQL 14 to 18. Manifests, commands, and defaults were checked against the CloudNativePG 1.30 documentation, the plugin documentation, and the release notes on 9 November 2026. They were not run in a lab, so rehearse them on a staging cluster first.


Which CloudNativePG version should you run in November 2026?

Run 1.30.x. The supported releases page lists 1.30 (released 29 June 2026) as supported until about March 2027, and 1.29 as ending on 29 September 2026. A minor release stays supported until three months after the next minor ships, and 1.31.0 is planned for around December 2026. CloudNativePG 1.30 supports Kubernetes 1.34, 1.35, and 1.36 and PostgreSQL 14 to 18, and its support for PostgreSQL 14 ends on 12 November 2026.

The project was accepted into the CNCF on 21 January 2025 and, according to the CNCF project page, moved to the Incubating maturity level on 2 November 2026.

bash
# Install the operator (the documented command uses server-side apply)
kubectl apply --server-side -f \
  https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.30/releases/cnpg-1.30.1.yaml
kubectl rollout status deployment -n cnpg-system cnpg-controller-manager

# Install the kubectl plugin with Krew
kubectl krew install cnpg

Interpretation: the rollout command returns once the controller is running in cnpg-system. We keep the kubectl cnpg plugin on the same minor version as the operator, so its commands match the API the operator serves.

The default image in 1.30.1 is 18.6-system-trixie. The system flavor only adds the in-tree Barman Cloud binaries and is deprecated, so pin a standard or minimal tag instead.

Should you run CloudNativePG or Patroni on VMs?

Both run PostgreSQL streaming replication. The difference is who owns leader election, routing, and backups. Patroni keeps cluster state in a distributed configuration store (etcd, Consul, ZooKeeper, or the Kubernetes API) and leaves routing and backups to tools you choose. CloudNativePG uses the Kubernetes API directly, manages PVCs itself instead of StatefulSets, and declares backups, poolers, and upgrades as resources.

CloudNativePG and Patroni on VMs side by side

Switch tabs to compare who handles each operational job

CloudNativePG 1.30

JobCloudNativePG 1.30
Leader electionKubernetes API; a per-cluster Lease gates promotion
Client routing-rw, -ro, and -r Services moved by the operator
Synchronous replication.spec.postgresql.synchronous with dataDurability
Backups and PITRBackup and ScheduledBackup resources through a CNPG-I plugin or volume snapshots
Planned switchoverkubectl cnpg promote
Major version upgradeDeclarative offline pg_upgrade by changing the image

Patroni on VMs

JobPatroni on VMs
Leader electionA DCS you run: usually a three-node etcd or Consul cluster
Client routingHAProxy health checks against the Patroni REST API, a VIP, or DNS
Synchronous replicationsynchronous_mode, synchronous_mode_strict, synchronous_node_count
Backups and PITRA separate tool you install and schedule, such as pgBackRest or WAL-G
Planned switchoverpatronictl switchover
Major version upgradeA pg_upgrade runbook you script around Patroni

Rule of thumb: if your team already runs stateful workloads on Kubernetes with tested StorageClasses, CloudNativePG removes the DCS, the proxy, and the backup scheduler. If not, Patroni on VMs keeps fewer layers between PostgreSQL and the disk.

Patroni's settings are in its replication modes documentation. For the VM side, see our posts on a production Patroni cluster and Patroni with Consul as the DCS.

What does a production Cluster manifest need?

The Cluster resource defines one primary and any number of replicas. The operator creates a -rw Service that always points at the primary, a -ro Service for replicas only, and an -r Service for any instance. Applications use the generated pg-prod-app Secret for credentials.

What one CloudNativePG Cluster creates

Applications reach PostgreSQL only through Services; the primary ships WAL to object storage

CloudNativePG Cluster architecture Applications connect to the pg-prod-rw Service, which routes to the primary instance, and to the pg-prod-ro Service, which routes to the replicas. The primary streams WAL to the replicas. The primary also archives WAL and base backups to object storage through the Barman Cloud Plugin. The CloudNativePG operator watches the Cluster resource and reconciles the pods, PVCs, and Services, moving the rw Service after a failover. pg-prod-rw Service: primary pg-prod-ro Service: replicas Primary pg-prod-1 Replicas pg-prod-2, pg-prod-3 Object storage WAL and base backups Operator reconciles the Cluster WAL archive streaming pods, PVCs

After a failover: the operator repoints pg-prod-rw at the promoted replica, so applications that connect through the Service, not a pod IP, follow the new primary without a config change.

A starting manifest for three instances spread across zones, with WAL on its own volume and backups through the plugin:

yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-prod
  namespace: prod
spec:
  instances: 3
  imageName: ghcr.io/cloudnative-pg/postgresql:18.6-standard-trixie
  primaryUpdateStrategy: unsupervised
  primaryUpdateMethod: switchover

  postgresql:
    parameters:
      max_connections: "200"
      shared_buffers: "2GB"
    synchronous:
      method: any
      number: 1
      dataDurability: required
      failoverQuorum: true

  resources:
    requests:
      cpu: "2"
      memory: 8Gi
    limits:
      cpu: "2"
      memory: 8Gi

  affinity:
    enablePodAntiAffinity: true
    topologyKey: topology.kubernetes.io/zone
    podAntiAffinityType: required

  storage:
    storageClass: fast-ssd
    size: 200Gi
  walStorage:
    storageClass: fast-ssd
    size: 50Gi

  plugins:
  - name: barman-cloud.cloudnative-pg.io
    isWALArchiver: true
    parameters:
      barmanObjectName: pg-prod-store

Interpretation: parameter values are strings. The anti-affinity default is preferred on kubernetes.io/hostname, which lets the scheduler co-locate instances when short of capacity. required on a zone key needs three schedulable zones, or a pod stays Pending. Equal requests and limits put the pods in the QoS class that the kubelet evicts last.

Storage and WAL volumes

The storage documentation gives the reasons for a separate WAL volume: WAL and data files stop competing for I/O, and a full PGDATA volume cannot block WAL writes. walStorage cannot be removed once added. Raising size resizes every PVC only if the StorageClass has allowVolumeExpansion: true; otherwise you re-create PVCs one instance at a time.

How do you configure synchronous replication?

Synchronous replication is off until you define .spec.postgresql.synchronous, and CloudNativePG does not let you set synchronous_standby_names directly. Two fields are mandatory: method (any for quorum, first for priority) and number. The replication documentation calls any the most common setup.

dataDurability decides what happens when standbys disappear. With required, the default, writes pause until enough synchronous standbys return. With preferred, the operator lowers the requested number to the healthy standbys it has, down to none, and writes continue with a risk of losing recent commits on a primary failure. preferred cannot be combined with standbyNamesPre or standbyNamesPost.

bash
kubectl cnpg psql pg-prod -n prod -- -c "SHOW synchronous_standby_names"

Interpretation: with the manifest above, expect ANY 1 ("pg-prod-2","pg-prod-3","pg-prod-1"). The primary's own name is listed last. An empty value means synchronous replication is not configured, or preferred mode has dropped every standby.

failoverQuorum: true adds a check. Under ANY 1, a commit is on the primary and one standby; if both fail together, the remaining replica may lack acknowledged transactions. With failover quorum, the operator promotes only when it can confirm the candidate holds all synchronously committed data. It replaces the deprecated alpha.cnpg.io/failoverQuorum annotation.

How do failover and switchover work?

When the primary stays unhealthy longer than .spec.failoverDelay (default 0 seconds), the operator shuts the old primary down so the WAL receivers stop, then promotes the most up-to-date replica. The old primary tries a fast shutdown first, bounded by .spec.switchoverDelay (default 3600 seconds), so pending WAL can be archived.

Since 1.30, a Kubernetes Lease named after the cluster serializes promotion. An instance must hold it to act as primary. After a crash, a replica must wait out leaseDurationSeconds (default 15) before it can promote. The failover documentation also puts node-level detection at about 40 to 55 seconds with stock kube-controller-manager settings, because the operator waits for the node lifecycle controller to mark the pod not ready.

bash
# Who holds the primary lease right now?
kubectl get lease pg-prod -n prod

# Planned switchover to instance 2 (for node maintenance or testing)
kubectl cnpg promote pg-prod pg-prod-2 -n prod

Interpretation: the HOLDER column names the current primary pod. promote performs a switchover: the former primary shuts down cleanly, releases the lease, and rejoins as a replica, while pg-prod-rw moves to the new primary. Expect clients to reconnect.

kubectl cnpg status on a healthy three-instance cluster

Illustrative output, shaped like the CloudNativePG 1.30 plugin; check archiving and roles first

kubectl: prod namespace
$ kubectl cnpg status pg-prod -n prodCluster SummaryName: prod/pg-prodPostgreSQL Image: ghcr.io/cloudnative-pg/postgresql:18.6-standard-trixiePrimary instance: pg-prod-1Status: Cluster in healthy state Continuous Backup status (Barman Cloud Plugin)Working WAL archiving: OKWALs waiting to be archived: 0Last Archived WAL: 00000002000000000000009A @ 2026-11-02T10:41:07Z Instances statusName Current LSN Replication role Status Nodepg-prod-1 0/9A0012F8 Primary OK node-apg-prod-2 0/9A0012F8 Standby (sync) OK node-bpg-prod-3 0/9A0012E0 Standby (sync) OK node-c

What to read first: a rising "WALs waiting to be archived" count means archiving is failing and pg_wal will fill. Quorum standbys also show as "Standby (sync)". See the kubectl plugin documentation.

How do you set up backups with the Barman Cloud Plugin?

Since 1.26, backup and recovery have been moving out of the operator into CNPG-I plugins. Removal of native Barman Cloud support has been postponed several times; the 1.30.0 release notes said 1.31.0, and the current backup documentation now says 1.31 is the last minor with native support, removed starting from 1.32.0. The default method on Backup resources is still barmanObjectStore, so set method: plugin explicitly.

The plugin needs cert-manager (or certificates you supply), must be installed in the operator's namespace, and version 0.15.1 requires CloudNativePG 1.26 or later. It runs as a sidecar in each instance pod. Its ObjectStore resource takes the same configuration as the old in-tree stanza, and retention moves there too:

yaml
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
  name: pg-prod-store
  namespace: prod
spec:
  retentionPolicy: "30d"
  configuration:
    destinationPath: s3://example-pg-backups/
    s3Credentials:
      accessKeyId:
        name: pg-backup-s3
        key: ACCESS_KEY_ID
      secretAccessKey:
        name: pg-backup-s3
        key: ACCESS_SECRET_KEY
    wal:
      compression: gzip

Interpretation: 30d is a recovery window. The plugin keeps the newest backup older than 30 days plus everything after it, so you can restore to any point in that window. Leave serverName empty here; set it as a plugin parameter in the Cluster when you need it.

Schedule base backups with a ScheduledBackup. Its cron expression has six fields, starting with seconds:

yaml
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: pg-prod-daily
  namespace: prod
spec:
  schedule: "0 0 2 * * *"   # 02:00:00 every day
  immediate: true
  backupOwnerReference: self
  cluster:
    name: pg-prod
  method: plugin
  pluginConfiguration:
    name: barman-cloud.cloudnative-pg.io

Interpretation: by default the backup runs on the most up-to-date standby (prefer-standby), away from the primary's I/O. immediate: true takes the first backup on creation. The cluster reference is immutable from 1.30.

Back up the Secrets separately

Backups do not include the superuser or application user Secrets. Cover them in your Kubernetes-level backups.

To migrate, follow the plugin's migration guide: build the ObjectStore from your old stanza, swap the stanza for the plugin in one change (a rolling update), then update every ScheduledBackup and externalClusters entry.

How do you restore to a point in time?

CloudNativePG never restores in place. Recovery bootstraps a new Cluster from a base backup and replays archived WAL up to a recoveryTarget, while the original cluster keeps running. If you give only targetTime, the operator picks the closest backup completed before that time. Timestamps without a time zone are read as UTC, so always write one.

yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-restore
  namespace: prod
spec:
  instances: 3
  imageName: ghcr.io/cloudnative-pg/postgresql:18.6-standard-trixie
  storage:
    storageClass: fast-ssd
    size: 200Gi
  walStorage:
    storageClass: fast-ssd
    size: 50Gi
  bootstrap:
    recovery:
      source: origin
      recoveryTarget:
        targetTime: "2026-11-02T09:58:00Z"
  externalClusters:
  - name: origin
    plugin:
      name: barman-cloud.cloudnative-pg.io
      parameters:
        barmanObjectName: pg-prod-store
        serverName: pg-prod

Interpretation: serverName points at the source cluster's folder in the bucket. Without a plugins section, the restored cluster does not archive WAL. If no transaction committed after the target time, recovery fails, as the recovery documentation warns.

PITR runbook: restore pg-prod to a new cluster

Seven steps; steps 5 and 6 are the write outage, only if you cut over

  1. Fix the target time

    Agree the last good moment with the application owner; write it in RFC 3339 with a zone.

  2. Confirm a backup and WAL cover it

    A completed backup must predate the target, and archiving must have been working since.

    kubectl get backups.postgresql.cnpg.io -n prod kubectl cnpg status pg-prod -n prod
  3. Apply the recovery Cluster

    Use a new name. The source cluster keeps serving traffic while the restore runs.

    kubectl apply -f pg-restore.yaml
  4. Validate the restored data

    Check the rows the incident affected before anyone decides to cut over.

    kubectl cnpg psql pg-restore -n prod -- app \ -c "SELECT max(updated_at) FROM orders"
  5. Stop writes to pg-prod

    Outage starts

    Pause the Pooler so PgBouncer finishes running queries and holds new ones.

    kubectl patch pooler pooler-pg-prod-rw -n prod \ --type merge -p '{"spec":{"paused":true}}'
  6. Point applications at pg-restore

    Outage ends

    A Pooler's cluster reference is immutable, so create a new Pooler for pg-restore or switch the connection string to pg-restore-rw.

  7. Turn on archiving and back up

    Add the plugins section to pg-restore and take a base backup, so it has its own recovery point.

    kubectl cnpg backup pg-restore -n prod \ --method=plugin \ --plugin-name=barman-cloud.cloudnative-pg.io

Rollback point: until step 6, pg-prod is untouched, so resuming the Pooler undoes the cutover. Keep pg-prod until the restored cluster has a completed backup of its own.

How do you add PgBouncer with the Pooler resource?

A Pooler is a Deployment of PgBouncer pods in front of one of the cluster's Services. It needs PgBouncer 1.19 or later, must live in the same namespace as the cluster, and cannot share a name with a cluster there. Deleting the Cluster does not delete its poolers. Authentication details are in the connection pooling documentation.

yaml
apiVersion: postgresql.cnpg.io/v1
kind: Pooler
metadata:
  name: pooler-pg-prod-rw
  namespace: prod
spec:
  cluster:
    name: pg-prod
  instances: 3
  type: rw
  pgbouncer:
    poolMode: transaction
    parameters:
      max_client_conn: "2000"
      default_pool_size: "40"

Interpretation: applications connect to the pooler-pg-prod-rw Service instead of pg-prod-rw. Each PgBouncer pod accepts up to 2,000 client connections and opens up to 40 server connections per user and database pair, so three pods can open 120 per pair. Size that against max_connections. Transaction mode breaks session state such as SET and advisory locks, so check the application first.

Setting paused: true runs PgBouncer's PAUSE, as in step 5 of the runbook above. Metrics are on port 9127; alert first on cnpg_pgbouncer_pools_cl_waiting.

How do minor and major PostgreSQL upgrades work?

For a minor upgrade, change imageName or the image catalog. The operator replaces replicas one at a time and the primary last; with primaryUpdateMethod: switchover, that last step is a switchover to an upgraded replica, so nothing waits on an image pull. With switchover, you cannot change the image and PostgreSQL parameters in the same edit: the operator rejects it with a validation error, so make them two changes.

For a major upgrade, CloudNativePG has supported a declarative offline in-place upgrade since 1.26. Change the major version in the image tag, keeping the same Debian release (a bookworm image cannot upgrade to trixie). The upgrade documentation describes the sequence: every pod shuts down, a pg-prod-1-major-upgrade Job runs pg_upgrade --link on the primary's volumes, then the replica PVCs are deleted and the replicas are re-cloned.

yaml
spec:
  imageName: ghcr.io/cloudnative-pg/postgresql:18.6-standard-trixie   # was 16.x-standard-trixie
  plugins:
  - name: barman-cloud.cloudnative-pg.io
    isWALArchiver: true
    parameters:
      barmanObjectName: pg-prod-store
      serverName: pg-prod-pg18   # new archive folder for the new major

Interpretation: pg_upgrade creates a new system identifier and restarts at timeline 1. The plugin does not separate archives on its own, so change serverName in the same edit to keep the old archive intact. If the Job fails, set the image back to the old major and the operator deletes the Job and restarts on the original data.

Plan the outage and the new backup

The whole cluster is unavailable during the upgrade, and re-cloning replicas takes time on large databases. Pre-upgrade backups cannot be used for PITR after it, so take a base backup straight away, and run ANALYZE because statistics are not carried over. On PostgreSQL 17.0 to 17.5, upgrades fail when max_slot_wal_keep_size is not -1; move to 17.6 first.

What should you monitor, and which kubectl cnpg commands matter?

Each instance exposes Prometheus metrics on port 9187, named metrics. The monitoring documentation recommends creating your own PodMonitor. The .spec.monitoring.enablePodMonitor field on Cluster and Pooler is deprecated. Query results are cached for 30 seconds by default.

yaml
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: pg-prod
  namespace: prod
spec:
  selector:
    matchLabels:
      cnpg.io/cluster: pg-prod
  podMetricsEndpoints:
  - port: metrics

Interpretation: the cnpg.io/cluster label selects every instance pod of pg-prod, whichever is primary at the time.

Start with three alerts. The metric names come from the operator's default monitoring queries; the thresholds are starting points to tune:

yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: pg-prod-alerts
  namespace: prod
spec:
  groups:
  - name: cloudnativepg
    rules:
    - alert: PostgresDown
      expr: cnpg_collector_up == 0
      for: 1m
    - alert: WalArchivingFailing
      expr: increase(cnpg_pg_stat_archiver_failed_count[10m]) > 0
      for: 10m
    - alert: ReplicaLagging
      expr: cnpg_pg_replication_lag > 60
      for: 5m

Interpretation: archiving failures matter most, because unarchived WAL stays in pg_wal until the volume fills. CloudNativePG sets archive_timeout to 5 minutes, which is where its documented recovery point objective of 5 minutes or less comes from. Also graph cnpg_collector_nodes_used: below the instance count, instances share a node.

Beyond status and promote, use kubectl cnpg report cluster pg-prod -n prod -f report.zip --logs to bundle manifests, events, and logs for a support ticket before you delete anything.

Frequently asked questions

Is the CloudNativePG barmanObjectStore backup method deprecated?

Yes. Native Barman Cloud support has been deprecated since CloudNativePG 1.26, and the current documentation says 1.31 is the last minor release that includes it, with removal starting from 1.32.0. Migrate to the Barman Cloud Plugin, which uses an ObjectStore resource and method: plugin on Backup and ScheduledBackup resources, before upgrading to 1.32.

Can CloudNativePG do a PostgreSQL major version upgrade in place?

Yes, since version 1.26. Change the major version in the cluster's image and the operator shuts down every pod, runs pg_upgrade with --link in a Job, and re-clones the replicas. It is an offline upgrade, so the whole cluster is unavailable during it. Take a new base backup afterwards, because older backups cannot be used for PITR across the upgrade.

Does CloudNativePG need etcd or Consul like Patroni?

No. CloudNativePG uses the Kubernetes API server to store cluster state and coordinate failover, and since 1.30 it uses a Kubernetes Lease per cluster to gate promotion. Patroni needs a distributed configuration store such as etcd, Consul, ZooKeeper, or the Kubernetes API, which you run and monitor yourself on VMs.

How do I trigger a switchover in CloudNativePG?

Run kubectl cnpg promote with the cluster name and the target instance, for example kubectl cnpg promote pg-prod pg-prod-2. The current primary shuts down cleanly, releases its lease, and rejoins as a replica, and the operator moves the rw Service to the new primary. Clients must reconnect.

Key takeaways

  • Run CloudNativePG 1.30.x with a pinned standard or minimal image, and keep the kubectl cnpg plugin on the same minor.
  • Set required anti-affinity on a zone or host key, a separate walStorage volume, and equal CPU and memory requests and limits before go-live.
  • Choose dataDurability deliberately: required pauses writes when standbys are missing; preferred keeps writing and can lose recent commits.
  • Move backups to the Barman Cloud Plugin now, schedule them with six-field cron, and rehearse PITR into a new cluster.
  • Treat major upgrades as a full-cluster outage: change serverName with the image, then take a new backup and run ANALYZE.

Run PostgreSQL on Kubernetes with JusDB

JusDB's PostgreSQL on Kubernetes team can review your CloudNativePG Cluster manifests, storage classes, and anti-affinity rules, plan a move from in-tree Barman Cloud to the plugin, and write and rehearse PITR and major upgrade runbooks with you.

Bring your manifests, operator and PostgreSQL versions, backup configuration, and agreed recovery targets. Your team approves every production change.

Share this article

Database engineering notes

Articles like this one, in your inbox. No spam, unsubscribe anytime.

JusDB Team

Official JusDB content team

Keep reading

REPACK CONCURRENTLY in PostgreSQL 19 vs pg_repack: Online Table Rebuilds

Learn what PostgreSQL 19's REPACK (CONCURRENTLY) locks, what it needs before it runs, how it compares with pg_repack, and when to keep the extension.

PostgreSQL19 minNov 9, 2026
Read

PostgreSQL Advisory Locks: Patterns and Pitfalls

Use PostgreSQL advisory locks safely: session vs transaction locks, key choice, singleton jobs, leader election, and the PgBouncer, LIMIT, and lock-table traps.

PostgreSQL18 minOct 27, 2026
Read

PostgreSQL Lock Waits: How to Find the Blocking Query and Fix It

Find the session behind a PostgreSQL lock wait with pg_blocking_pids, see why a waiting ALTER TABLE stalls every SELECT, and choose between cancel, terminate, and timeouts.

PostgreSQL18 minOct 27, 2026
Read