Replication and High Availability
PostgreSQL replicates by shipping its write-ahead log (WAL) from a primary to one or more replicas. Replicas can serve read-only queries (read scaling) and stand ready for promotion if the primary fails (high availability).
| Mode | Behavior |
|---|---|
| Asynchronous streaming (default) | Primary doesn't wait for replicas; tiny risk of losing the last transactions on failover. Lowest latency. |
| Synchronous streaming | Primary waits for at least one replica to confirm before committing; no data loss on failover, higher commit latency. |
| Logical replication | Replicates selected tables via publish/subscribe; enables cross-version and partial replication. |
Streaming Replication with Docker Compose
1. Prepare the primary. Set replication parameters and create a replication role:
# primary postgresql.conf
wal_level = replica
max_wal_senders = 10
wal_keep_size = 512MB # retain WAL so a lagging replica can catch up
hot_standby = on
-- on the primary
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'repl_secret';
# primary pg_hba.conf — allow the replica to connect for replication
host replication replicator 10.0.0.0/8 scram-sha-256
2. Seed and start the replica. A replica is initialized from a base backup of the primary:
# In the replica container, clone the primary's data directory
pg_basebackup -h primary -U replicator -D /var/lib/postgresql/data \
-Fp -Xs -P -R
# -R writes standby.signal + primary_conninfo automatically
The -R flag creates standby.signal and sets primary_conninfo, so the replica starts following the primary on boot. A minimal Compose sketch:
services:
primary:
image: postgres:17
environment:
POSTGRES_PASSWORD: changeme
volumes:
- primary-data:/var/lib/postgresql/data
- ./primary/postgresql.conf:/etc/postgresql/postgresql.conf:ro
- ./primary/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
command: ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"]
replica:
image: postgres:17
depends_on: [ primary ]
environment:
PGPASSWORD: repl_secret
volumes:
- replica-data:/var/lib/postgresql/data
# Entrypoint should pg_basebackup from "primary" on first start, then run postgres
# (use an init script or a small wrapper image)
volumes:
primary-data:
replica-data:
Verify replication health:
-- on the primary: see connected replicas and their lag
SELECT client_addr, state, sent_lsn, replay_lsn,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes
FROM pg_stat_replication;
-- on the replica: confirm it is in recovery
SELECT pg_is_in_recovery(); -- true
For zero-data-loss commits, make one replica synchronous on the primary:
synchronous_standby_names = 'FIRST 1 (replica1)'
synchronous_commit = on
Connection Pooling (PgBouncer)
PostgreSQL uses one process per connection, so thousands of app connections are expensive. Put PgBouncer in front to multiplex many client connections onto a small pool of server connections:
services:
pgbouncer:
image: edoburu/pgbouncer:latest
environment:
DATABASE_URL: "postgres://appuser:changeme@db:5432/appdb"
POOL_MODE: transaction # return the server conn after each transaction
MAX_CLIENT_CONN: 1000
DEFAULT_POOL_SIZE: 25 # server connections per user/db
ports:
- "6432:6432"
depends_on: [ db ]
| Pool mode | Reuses a server connection after… | Use with |
|---|---|---|
session |
the client disconnects | Anything (safest; least pooling benefit) |
transaction |
each transaction | Most web apps — big win; avoid session-level features (some prepared statements, SET, advisory locks held across transactions) |
statement |
each statement | Autocommit-only workloads |
Tip
Point applications at PgBouncer (port 6432), not directly at PostgreSQL. Keep PostgreSQL's max_connections modest (e.g. 100–200) and let PgBouncer absorb client concurrency. This is often the single biggest scalability improvement for a busy database.
High Availability (Automatic Failover)
Plain streaming replication does not promote a replica automatically — you need an HA layer that detects failure, promotes a replica, and redirects clients:
| Tool | Model | Notes |
|---|---|---|
| Patroni | Template + DCS (etcd/Consul/ZooKeeper) | The de-facto standard for self-managed HA; handles leader election, promotion, and reconfiguration |
| repmgr | Replication manager + daemon | Simpler than Patroni; manual or automated failover |
| CloudNativePG | Kubernetes operator | Declarative clusters, failover, backups, and rolling upgrades as Kubernetes resources |
| Zalando postgres-operator / Crunchy PGO | Kubernetes operators (Patroni-based) | Mature operators with backups, connection pooling, and monitoring built in |
Important
On Kubernetes, prefer a PostgreSQL operator (CloudNativePG, Crunchy PGO, Zalando) over hand-built StatefulSets. Operators encode the hard parts — failover, fencing to avoid split-brain, base-backup seeding of new replicas, WAL archiving, and safe rolling upgrades — that are easy to get dangerously wrong by hand.
Choosing an Approach
- Single container — fine for development and many small production workloads with good backups.
- Primary + async replica(s) — read scaling and a warm standby for manual failover.
- Synchronous replication — when you cannot lose committed transactions.
- Patroni or an operator — when you need automatic failover with minimal downtime.