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.
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:
- It avoids vendor lock-in. The same PostgreSQL cluster runs on AWS, GCP, Azure, or bare metal, so your data and configuration stay portable.
- 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.
- 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.
| Pooler | What it does | Typical mode |
|---|---|---|
| PgBouncer | Connection pooling, lightweight | Transaction |
| pgpool-II | Connection pooling plus read routing to replicas | Transaction |
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.
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: postgresMulti-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: DoNotSchedule3. 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: 4000mBurstable: the first target for the OOM killer under memory pressure.
resources:
requests:
memory: 8Gi
cpu: 4000m
limits:
memory: 8Gi
cpu: 4000mGuaranteed: protected by kernel priority.
| QoS class | When a pod gets it |
|---|---|
| Guaranteed | Requests equal limits |
| Burstable | Requests lower than limits |
| BestEffort | No 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 | |
|---|---|---|
| Latency | About 1 ms | About 0.1 ms |
| Throughput | Medium | High |
| Node failure | Data survives, and the volume re-attaches to the new pod right away | Data on the node is lost, and the replica needs a full resync |
| Cost | Provisioned IOPS add cost | Better cost per IOPS |
| Best for | General workloads and simplicity | Write-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: /metricsPostgreSQL-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:
| Metric | Source | What to watch |
|---|---|---|
| Replication lag (bytes) | pg_stat_replication, pg_replication_slots | Bytes behind the primary. Alert if lag keeps growing. |
| Autovacuum activity | pg_stat_user_tables | Dead tuples piling up without autovacuum lead to table bloat and transaction ID wraparound. A silent killer. |
| Cache hit ratio | pg_statio_user_tables, pg_stat_database | heap_blks_hit / (heap_blks_hit + heap_blks_read) should be above 99%. Below 95% suggests shared_buffers is too small. |
Connections vs max_connections | pg_stat_activity | Active, idle, and idle-in-transaction connections. Alert before you reach max_connections. |
| Lock waits and deadlocks | pg_locks, pg_stat_database | Deadlocks and long lock waits point to contention in the application. Investigate them right away. |
Adapting monitoring for Kubernetes
- Use
ServiceMonitororPodMonitorand 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_waitingcounter 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.
DROP TABLE.Backup architecture decisions
| Approach | Setup | Why |
|---|---|---|
| Recommended | pgBackRest with S3 in a separate cloud account or region, with WAL archiving enabled for full PITR | Survives the loss of the cluster. Test restores regularly, because an untested backup is not a backup. |
| Acceptable for dev and staging | S3-compatible storage such as MinIO, RustFS, or Ceph running in the same Kubernetes cluster | Convenient, but it shares the cluster's fate. If you lose the cluster, you lose the backups. |
| Not recommended | Base backups written to local PVCs | No 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.
| Operator | Maintainer | License | HA method |
|---|---|---|---|
| CloudNativePG | EDB / CNCF | Apache 2.0 | Built in, no Patroni required |
| Zalando Postgres Operator | Zalando | MIT | Patroni with etcd |
| Percona Operator for PostgreSQL | Percona | Apache 2.0 | Patroni, with pgBackRest backups |
| StackGres | OnGres | AGPL 3.0, or a commercial license | Patroni, with bundled PgBouncer |
| Crunchy Data PGO | Crunchy Data (Snowflake) | Apache 2.0 | Patroni, with pgBackRest backups. No longer actively developed |
| KubeDB | AppsCode | Proprietary | Configurable |
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
- The Cloud-Native Data Manual: storage, compute, FinOps, security, and availability for production databases on Kubernetes.
- Running MongoDB on Kubernetes: replica sets, sharded clusters, mongos routing, PBM backups, and operator comparison.
- Running MySQL on Kubernetes: InnoDB Cluster, Group Replication, XtraBackup, and operator selection.
- Running SQL Server on Kubernetes: availability groups without a Windows cluster, memory and licensing-aware sizing, native backups to S3, and operator comparison.
- Running Valkey on Kubernetes: replication and Valkey Cluster, the persistence trap, maxmemory and container limits, RDB and AOF, and operator comparison.
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.