PostgreSQL
Kubernetes
Streaming Replication
Database Management
Cloud Infrastructure

How to enable streaming replication in PostgreSQL running in kubernetes pods?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

PostgreSQL streaming replication keeps a standby server synchronized with a primary by continuously shipping and replaying WAL (Write-Ahead Log) records. Running this inside Kubernetes requires StatefulSets for stable pod identities, ConfigMaps for PostgreSQL configuration, and careful networking between primary and replica pods. For production use, consider an operator like CloudNativePG or Zalando's postgres-operator, but understanding the manual setup clarifies what the operators automate.

Prerequisites

Before starting, you need a running Kubernetes cluster with kubectl configured, a PostgreSQL Docker image (the official postgres:16 image works), and a basic understanding of StatefulSets and persistent volumes.

Step 1: Create a ConfigMap for PostgreSQL Settings

The primary server needs WAL-level settings that enable replication.

yaml
1apiVersion: v1
2kind: ConfigMap
3metadata:
4  name: postgres-config
5data:
6  postgresql.conf: |
7    listen_addresses = '*'
8    wal_level = replica
9    max_wal_senders = 5
10    wal_keep_size = 256MB
11    hot_standby = on
12  pg_hba.conf: |
13    local   all   all                 trust
14    host    all   all   0.0.0.0/0     md5
15    host    replication  replicator  0.0.0.0/0  md5

wal_level = replica enables streaming replication. max_wal_senders controls how many standby connections the primary accepts. The pg_hba.conf entry allows a replicator user to connect for replication from any pod in the cluster.

Step 2: Create a Secret for Replication Credentials

yaml
1apiVersion: v1
2kind: Secret
3metadata:
4  name: postgres-secret
5type: Opaque
6stringData:
7  POSTGRES_PASSWORD: "primary-pass"
8  REPLICATION_PASSWORD: "replica-pass"

Step 3: Deploy the Primary with a StatefulSet

yaml
1apiVersion: apps/v1
2kind: StatefulSet
3metadata:
4  name: postgres-primary
5spec:
6  serviceName: postgres-primary
7  replicas: 1
8  selector:
9    matchLabels:
10      app: postgres
11      role: primary
12  template:
13    metadata:
14      labels:
15        app: postgres
16        role: primary
17    spec:
18      containers:
19        - name: postgres
20          image: postgres:16
21          ports:
22            - containerPort: 5432
23          env:
24            - name: POSTGRES_PASSWORD
25              valueFrom:
26                secretKeyRef:
27                  name: postgres-secret
28                  key: POSTGRES_PASSWORD
29            - name: PGDATA
30              value: /var/lib/postgresql/data/pgdata
31          volumeMounts:
32            - name: data
33              mountPath: /var/lib/postgresql/data
34            - name: config
35              mountPath: /etc/postgresql/conf.d
36      volumes:
37        - name: config
38          configMap:
39            name: postgres-config
40  volumeClaimTemplates:
41    - metadata:
42        name: data
43      spec:
44        accessModes: ["ReadWriteOnce"]
45        resources:
46          requests:
47            storage: 10Gi
48---
49apiVersion: v1
50kind: Service
51metadata:
52  name: postgres-primary
53spec:
54  clusterIP: None
55  selector:
56    app: postgres
57    role: primary
58  ports:
59    - port: 5432

A headless service (clusterIP: None) gives each pod a stable DNS name like postgres-primary-0.postgres-primary.

Step 4: Create the Replication User

Connect to the primary pod and create the replication user:

bash
kubectl exec -it postgres-primary-0 -- psql -U postgres -c \
  "CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'replica-pass';"

Step 5: Deploy the Replica

The replica uses pg_basebackup to initialize from the primary, then streams WAL changes continuously.

yaml
1apiVersion: apps/v1
2kind: StatefulSet
3metadata:
4  name: postgres-replica
5spec:
6  serviceName: postgres-replica
7  replicas: 1
8  selector:
9    matchLabels:
10      app: postgres
11      role: replica
12  template:
13    metadata:
14      labels:
15        app: postgres
16        role: replica
17    spec:
18      initContainers:
19        - name: init-replica
20          image: postgres:16
21          command:
22            - bash
23            - -c
24            - |
25              if [ ! -f /var/lib/postgresql/data/pgdata/PG_VERSION ]; then
26                PGPASSWORD=replica-pass pg_basebackup \
27                  -h postgres-primary-0.postgres-primary \
28                  -U replicator -D /var/lib/postgresql/data/pgdata \
29                  -Fp -Xs -R
30              fi
31          volumeMounts:
32            - name: data
33              mountPath: /var/lib/postgresql/data
34      containers:
35        - name: postgres
36          image: postgres:16
37          ports:
38            - containerPort: 5432
39          env:
40            - name: PGDATA
41              value: /var/lib/postgresql/data/pgdata
42          volumeMounts:
43            - name: data
44              mountPath: /var/lib/postgresql/data
45  volumeClaimTemplates:
46    - metadata:
47        name: data
48      spec:
49        accessModes: ["ReadWriteOnce"]
50        resources:
51          requests:
52            storage: 10Gi

The -R flag in pg_basebackup creates a standby.signal file and writes connection info to postgresql.auto.conf, so the replica automatically connects to the primary for streaming.

Step 6: Verify Replication

bash
1# On the primary — check connected replicas
2kubectl exec -it postgres-primary-0 -- psql -U postgres -c \
3  "SELECT client_addr, state, sent_lsn, replay_lsn FROM pg_stat_replication;"
4
5# On the replica — confirm standby mode
6kubectl exec -it postgres-replica-0 -- psql -U postgres -c \
7  "SELECT pg_is_in_recovery();"
8# Should return: t (true)

Common Pitfalls

  • Forgetting to set wal_level = replica on the primary — without this, replicas cannot connect.
  • Not creating the replication user with the REPLICATION privilege — pg_basebackup fails with permission denied.
  • Using a Deployment instead of a StatefulSet — Deployments do not provide stable network identities or persistent storage across restarts.
  • Skipping the pg_hba.conf replication entry — the primary rejects replication connections even with valid credentials.
  • Not monitoring replication lag — use pg_stat_replication on the primary to detect replicas falling behind.

Summary

  • Use StatefulSets with headless services for stable pod DNS names.
  • Configure wal_level = replica and pg_hba.conf replication entries on the primary.
  • Initialize replicas with pg_basebackup -R to auto-configure streaming connection.
  • Verify replication with pg_stat_replication on the primary and pg_is_in_recovery() on the replica.
  • For production, consider PostgreSQL operators (CloudNativePG, Zalando) that automate failover and backup management.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.