Whitepapers / PostgreSQL on Kubernetes

Running PostgreSQL on Kubernetes

A complete architecture guide

PostgreSQL is the world's most advanced open source relational database. Running it on Kubernetes is no longer a question of whether, but of how.

For DBAs, SREs, and platform engineers. Covers PostgreSQL 14 and later.

1. Introduction: PostgreSQL meets Kubernetes

PostgreSQL has been in continuous development since 1986 and is widely regarded as the most advanced open source relational database. Running it on Kubernetes is no longer experimental. It is a production-proven approach used by organizations that need portability, automation, and freedom from vendor lock-in.

Is PostgreSQL on Kubernetes production-ready?

Yes. CloudNativePG, the leading PostgreSQL operator, is a CNCF Sandbox project with thousands of production deployments. Patroni, the high-availability layer used by operators such as Zalando and Percona, has been running PostgreSQL clusters in production since 2015, at companies including Zalando itself, GitLab, and Netflix.

Compared with VM-based deployments, running PostgreSQL on Kubernetes has three main advantages:

  1. It avoids vendor lock-in. The same PostgreSQL cluster runs on AWS, GCP, Azure, or bare metal, so your data and configuration stay portable.
  2. Operations are automated. Operators handle failover, scaling, backups, and upgrades. Patroni's DCS-based leader election makes failover deterministic and fast, typically under 30 seconds.
  3. It costs less. You can consolidate workloads on shared clusters, right-size resources, and avoid the premium of managed services like RDS or Cloud SQL, often saving 40–60% on total cost of ownership.

With an active Data on Kubernetes community, mature operators, and years of production use, PostgreSQL on Kubernetes has proven itself at scale. The real decisions are which topology, storage strategy, and operator to use.

2. PostgreSQL on Kubernetes architectures

A PostgreSQL deployment on Kubernetes has two layers: a connection pooling layer that deals with PostgreSQL's process-per-connection model, and a database layer with streaming replication and automated failover.

The connection pooling layer (PgBouncer, pgpool-II)

Unlike MySQL, which uses threads, PostgreSQL starts a new OS process for every client connection. At scale, hundreds or thousands of direct connections use gigabytes of RAM and put pressure on the scheduler of the database server. In production, connection pooling is essential.

PgBouncer is the standard choice: a lightweight, single-binary pooler that supports session, transaction, and statement pooling. Transaction pooling, the most common mode, lets hundreds of application connections share a small pool of real server connections, which greatly reduces PostgreSQL's resource use.

pgpool-II is a heavier alternative that also handles read/write splitting, sending SELECT queries to replicas and writes to the primary. It is more complex to configure, but useful when you need read scaling without changing the application.

PoolerWhat it doesTypical mode
PgBouncerConnection pooling, lightweightTransaction
pgpool-IIConnection pooling plus read routing to replicasTransaction

Asynchronous streaming replication

This is the standard PostgreSQL replication model. The primary streams WAL (write-ahead log) records to standby nodes in real time, and the standbys apply them continuously, staying close behind the primary. If the primary fails, Patroni automatically promotes the most up-to-date standby.

The trade-off is a small replication lag. If the primary fails before a standby catches up, a small number of committed transactions can be lost. Setting synchronous_commit = remote_write gives a middle ground similar to semi-synchronous replication.

Logical replication, a secondary topology, replicates at the row level. It is useful for zero-downtime major version upgrades and for replicating selected tables, but it is not a primary HA mechanism.

Synchronous streaming replication

For zero data loss, PostgreSQL supports fully synchronous replication through synchronous_standby_names. The primary waits for the configured standbys to acknowledge each WAL record before it reports the transaction as committed.

This guarantees that at least one standby always has every committed transaction. The cost is higher write latency, because each write waits for a round trip to another node. In synchronous mode, Patroni only promotes standbys that are fully caught up.

Kubernetes cluster: 3 nodes across 3 availability zonesZone A · node-1Zone B · node-2Zone C · node-3PgBouncerPgBouncerPgBouncerPrimary (read/write)Standby (read-only)Standby (read-only)etcd memberetcd memberetcd memberPersistent volumePersistent volumePersistent volumepostgres-0postgres-1postgres-2WAL streaming
A three-node cluster across three zones. Each zone runs a PgBouncer pod, one PostgreSQL instance, an etcd member for Patroni's leader election, and its own persistent volume. The primary streams WAL to both standbys, and strict pod anti-affinity keeps the instances on separate nodes.

High availability with Patroni

Patroni uses a distributed configuration store (DCS) such as etcd, Consul, or ZooKeeper for leader election and failover. When the primary goes down, Patroni promotes the most advanced standby, typically in under 30 seconds.

CloudNativePG takes a different approach. It implements HA without Patroni, using Kubernetes-native leader election and its own failover controller. Both approaches give the same result: automatic, deterministic promotion with no manual steps.

In both cases, keep PostgreSQL pods on separate nodes with pod anti-affinity:

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    - topologyKey: kubernetes.io/hostname
      labelSelector:
        matchLabels:
          app: postgres

Multi-AZ deployments

A three-node PostgreSQL cluster (one primary and two standbys) with matching PgBouncer pods and a three-member etcd ring needs at least three Kubernetes nodes in three availability zones. Use topologySpreadConstraints to spread the database pods evenly, so the cluster, including the DCS quorum, survives the loss of a whole zone.

topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule

3. Compute and storage strategy

Getting resources right is the difference between a solid database and one that the Linux OOM killer stops at 3 AM. For PostgreSQL, storage choices also directly affect WAL throughput and recovery time.

Guaranteed QoS (requests equal limits)

For stateless apps, low requests and high limits are a standard way to pack more pods onto each node. For databases, this causes problems. If the memory limit is higher than the request, the Linux OOM killer targets the database pod first when the node runs low on memory.

For PostgreSQL on Kubernetes, set requests equal to limits for both CPU and memory. This puts the pod in the Guaranteed QoS class, which has the highest priority and is the last to be evicted when a node is under pressure.

resources:
  requests:
    memory: 2Gi
    cpu: 500m
  limits:
    memory: 8Gi
    cpu: 4000m

Burstable: the first target for the OOM killer under memory pressure.

resources:
  requests:
    memory: 8Gi
    cpu: 4000m
  limits:
    memory: 8Gi
    cpu: 4000m

Guaranteed: protected by kernel priority.

QoS classWhen a pod gets it
GuaranteedRequests equal limits
BurstableRequests lower than limits
BestEffortNo requests or limits set

shared_buffers and WAL tuning

PostgreSQL's shared_buffers parameter sets how much memory is used for caching data pages. The usual recommendation is 25% of available RAM. In a Guaranteed QoS pod, set the container memory limit high enough for shared_buffers plus working memory, autovacuum workers, and connection overhead.

WAL writes are sequential, so WAL performance depends more on write latency than on IOPS. For write-heavy workloads, putting the WAL directory (pg_wal) on a separate low-latency volume, ideally local NVMe, improves throughput significantly.

Network-attached storage (EBS, Ceph)

The standard approach is to separate storage from compute with network-attached volumes such as AWS EBS, GCP Persistent Disk, or Ceph. When a node fails, the pod is rescheduled on another node and the same PersistentVolume is re-attached with all its data, so no resynchronization is needed.

This makes moving pods simple and fast. The trade-off is I/O latency, which is higher than local disk because every read and write goes over the network. For most PostgreSQL workloads, network storage with provisioned IOPS is the right default.

Local NVMe SSDs

For latency-sensitive workloads, local NVMe SSDs give the best raw performance, with much lower I/O latency and higher throughput than any network-attached option. This matters most for WAL writes and random page reads.

The trade-off is durability. If the node fails, the local volume is lost, and the rescheduled standby has to resynchronize the full dataset through streaming replication. Make sure at least one standby uses network-attached storage and that pgBackRest backups are current.

Network-attached (EBS)Local NVMe SSD
LatencyAbout 1 msAbout 0.1 ms
ThroughputMediumHigh
Node failureData survives, and the volume re-attaches to the new pod right awayData on the node is lost, and the replica needs a full resync
CostProvisioned IOPS add costBetter cost per IOPS
Best forGeneral workloads and simplicityWrite-heavy, latency-sensitive workloads

4. Monitoring PostgreSQL on Kubernetes

Kubernetes changes how monitoring works. Pods come and go and their IPs change, so the monitoring stack has to discover targets dynamically. Prometheus Operator with postgres_exporter is the production standard for PostgreSQL on Kubernetes.

Prometheus and Grafana

The usual stack is Prometheus Operator, postgres_exporter, and Grafana. postgres_exporter connects to PostgreSQL and exposes hundreds of metrics from the pg_stat_* views, including replication lag, autovacuum activity, connection counts, and cache hit ratios.

Prometheus Operator adds the ServiceMonitor and PodMonitor custom resources, which tell Prometheus which pods to scrape. Because pod IPs change in Kubernetes, you never hardcode scrape targets.

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
spec:
  selector:
    matchLabels:
      app: postgres
  endpoints:
  - port: metrics
    path: /metrics

PostgreSQL-specific metrics

Generic Kubernetes metrics such as CPU, memory, and network tell you the pod is alive. PostgreSQL metrics tell you whether the database is healthy. Instrument these five first:

MetricSourceWhat to watch
Replication lag (bytes)pg_stat_replication, pg_replication_slotsBytes behind the primary. Alert if lag keeps growing.
Autovacuum activitypg_stat_user_tablesDead tuples piling up without autovacuum lead to table bloat and transaction ID wraparound. A silent killer.
Cache hit ratiopg_statio_user_tables, pg_stat_databaseheap_blks_hit / (heap_blks_hit + heap_blks_read) should be above 99%. Below 95% suggests shared_buffers is too small.
Connections vs max_connectionspg_stat_activityActive, idle, and idle-in-transaction connections. Alert before you reach max_connections.
Lock waits and deadlockspg_locks, pg_stat_databaseDeadlocks and long lock waits point to contention in the application. Investigate them right away.

Adapting monitoring for Kubernetes

  • Use ServiceMonitor or PodMonitor and never hardcode Prometheus scrape targets.
  • Monitor the Patroni health endpoint (/health) separately from database metrics.
  • Alert on Patroni leader changes, since unexpected promotions point to instability.
  • Track PVC capacity. WAL can fill a volume faster than you expect under heavy writes.
  • Monitor PgBouncer pool saturation. The cl_waiting counter shows clients queued for a server connection.
  • Start from the community PostgreSQL Database dashboard (9628) in Grafana.

5. Backup strategies

Backups are the last line of defence. In cloud-native environments, the standard approach is S3-compatible object storage combined with continuous WAL archiving, which lets you recover to any second in your retention window.

S3-compatible backup storage

The cloud-native standard for backup storage is any S3-compatible endpoint: AWS S3, Google Cloud Storage, Azure Blob through the S3 API, or self-hosted options such as MinIO. These give you durability, versioning, and lifecycle management.

Keep backups outside the Kubernetes cluster: in a separate account or region, or at the very least a separate namespace. If the cluster is compromised or destroyed, your backups survive.

pgBackRest is the most widely used backup tool for PostgreSQL on Kubernetes. It supports S3 natively, handles compression and encryption, and is built into the Percona Operator and Crunchy Data PGO. CloudNativePG uses Barman Cloud instead, through its backup plugin.

Point-in-time recovery (WAL archiving)

A full base backup is a snapshot of one moment. Real incidents often need a restore to an exact second, such as just before a bad DROP TABLE or a data corruption event. This is point-in-time recovery (PITR), and PostgreSQL supports it natively through continuous WAL archiving.

The WAL records every change in order. With WAL segments archived continuously to S3, you can replay them on top of any base backup and stop at any moment, to the second. It is the most powerful recovery tool PostgreSQL has.

WAL segments archived continuously to S3Base backupBase backupMon 00:00Thu 00:00Restore to Wed 14:31DROP TABLE at Wed 14:32
Recovery replays archived WAL on top of Monday's base backup and stops one minute before the bad DROP TABLE.

Backup architecture decisions

ApproachSetupWhy
RecommendedpgBackRest with S3 in a separate cloud account or region, with WAL archiving enabled for full PITRSurvives the loss of the cluster. Test restores regularly, because an untested backup is not a backup.
Acceptable for dev and stagingS3-compatible storage such as MinIO, RustFS, or Ceph running in the same Kubernetes clusterConvenient, but it shares the cluster's fate. If you lose the cluster, you lose the backups.
Not recommendedBase backups written to local PVCsNo durability guarantees, no versioning, no WAL archiving, and the same blast radius as the database.

6. PostgreSQL operators compared

The Kubernetes ecosystem has several mature PostgreSQL operators, each with a different approach to HA, backups, and licensing.

OperatorMaintainerLicenseHA method
CloudNativePGEDB / CNCFApache 2.0Built in, no Patroni required
Zalando Postgres OperatorZalandoMITPatroni with etcd
Percona Operator for PostgreSQLPerconaApache 2.0Patroni, with pgBackRest backups
StackGresOnGresAGPL 3.0, or a commercial licensePatroni, with bundled PgBouncer
Crunchy Data PGOCrunchy Data (Snowflake)Apache 2.0Patroni, with pgBackRest backups. No longer actively developed
KubeDBAppsCodeProprietaryConfigurable

CloudNativePG

A CNCF Sandbox project and the fastest-growing PostgreSQL operator. It implements HA natively with Kubernetes leader election, so no external DCS such as etcd or Consul is needed. Backups and WAL archiving go to object storage through the Barman Cloud plugin.

Zalando Postgres Operator

Used in production at Zalando for years. It relies on Patroni with etcd for leader election and automatic failover, is well documented and widely deployed, and has a large community. Spilo is the underlying PostgreSQL image.

Percona Operator for PostgreSQL

A fully open source operator from Percona, built on Patroni for HA and pgBackRest for backups to S3. It includes PgBouncer integration and support for Percona Monitoring and Management (PMM).

StackGres

A full-stack PostgreSQL distribution that bundles Patroni, PgBouncer, backups, and connection pooling into one operator. It focuses on extensibility, with a large set of PostgreSQL extensions packaged in. It is licensed under AGPL 3.0, with a commercial license available from OnGres.

Crunchy Data PGO

An enterprise-grade open source operator with a strong focus on security: TLS everywhere, Pod Security, and NetworkPolicy. It is built around Patroni and pgBackRest and is used in OpenShift environments in government and finance. Since Crunchy Data was acquired by Snowflake in 2025, it is no longer actively developed and only gets maintenance releases.

KubeDB

Part of a multi-database operator suite (MySQL, MongoDB, Redis, and others) with one management plane. It is not open source, and enterprise features need a license. It is convenient if you want one operator for many database types.

OpenEverest

OpenEverest is a CNCF Sandbox project for running databases on Kubernetes through one control plane. For PostgreSQL it offers two providers: one based on CloudNativePG and one based on the Percona Operator for PostgreSQL. Either way you get high availability, point-in-time recovery to S3-compatible storage, and connection pooling from the same UI and API.

OpenEverest is modular, so the operator underneath is pluggable. The same approach covers MySQL, MongoDB, and a growing list of other databases. You learn one API and one way of operating, whichever operator or database runs underneath.

Related whitepapers

Run PostgreSQL on Kubernetes with Solanica

Solanica builds on OpenEverest to give you high availability, point-in-time recovery, and connection pooling for PostgreSQL through one open source control plane, on infrastructure you own. Talk to us about your setup, or try OpenEverest yourself.