DISTRIBUTED MINDS · Postgres Replication Setup ← Back to Database Scaling
Hands-on setup guide

From two blank machines
to a working primary + replica.

The replication demo showed you what a leader and follower do conceptually. This page is the other half: the actual commands and config files that turn two real (or containerized) Postgres servers into that setup — in order, with the reason for each step.

7setup steps
2machines
1Docker quickstart
db-primary-01
10.0.1.10
db-replica-01
10.0.1.11
the same two machines from the replication demo — now being built step by step
Ch.01

The setup, step by step

Click a step below. The diagram highlights exactly what changes on which machine, and the panel shows the real command or config for it.

db-primary-01
10.0.1.10 · role: primary
listen_addresses = '*'
pg_hba.conf — allow 10.0.1.11
postgresql.conf — wal_level=replica
role: replicator (REPLICATION LOGIN)
replication slot: replica_01_slot
WAL sender process
db-replica-01
10.0.1.11 · role: standby
data directory — empty → cloned
standby.signal
postgresql.auto.conf — primary_conninfo
WAL receiver process
role: standby (read-only)
Ch.02

Before you do this for real

The 7 steps get you a working replica. These three decisions determine whether it survives production.

Sync vs. async replication pick one

By default this setup is asynchronous — the primary commits and returns to the client without waiting for the replica to confirm it received the WAL. Fast, but a crash right after commit can lose the last few transactions. Toggle it below.

commit returns~1ms — doesn't wait for replica

Async (default): synchronous_standby_names is unset. The primary never blocks on a replica — fastest writes, but failover can lose the most recent commits.

Failover not automatic

Nothing in this setup promotes the replica automatically if the primary dies. Manually, it's one command on the replica: pg_ctl promote — that stops it replaying WAL and starts accepting writes.

For automatic failover you need something watching both nodes and re-pointing traffic: Patroni or repmgr if self-hosting, or a managed service (RDS, Cloud SQL, Aurora) if you'd rather not build that yourself.

Ch.03

Try it locally first — Docker Compose

Fastest way to see the whole thing work end-to-end without provisioning real machines: two containers on one Docker network, doing exactly the 7 steps above automatically on startup. Local testing only — the replica's entrypoint script and open trust auth are not production-safe.

docker-compose.yml
version: "3.8"
services:
  primary:
    image: postgres:16
    container_name: pg-primary
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: appdb
    command: >
      postgres -c wal_level=replica -c max_wal_senders=10
               -c max_replication_slots=10 -c hot_standby=on
    volumes:
      - ./primary-data:/var/lib/postgresql/data
      - ./init-primary.sql:/docker-entrypoint-initdb.d/init-primary.sql
    ports: ["5432:5432"]
    networks: [pgnet]

  replica:
    image: postgres:16
    container_name: pg-replica
    depends_on: [primary]
    environment:
      PGPASSWORD: strongpassword
    entrypoint: >
      bash -c "
      until pg_isready -h primary -U replicator; do sleep 1; done;
      rm -rf /var/lib/postgresql/data/*;
      pg_basebackup -h primary -U replicator -D /var/lib/postgresql/data
        -Fp -Xs -P -R --slot=replica_01_slot;
      chmod 700 /var/lib/postgresql/data;
      exec docker-entrypoint.sh postgres"
    volumes:
      - ./replica-data:/var/lib/postgresql/data
    ports: ["5433:5432"]
    networks: [pgnet]

networks:
  pgnet: {}
init-primary.sql — runs once, automatically, on the primary's first boot
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'strongpassword';
-- open pg_hba.conf trust for the docker subnet so the replica container can connect;
-- for anything beyond a local test, scope this to the replica's IP and use scram-sha-256.
SELECT pg_create_physical_replication_slot('replica_01_slot');
run it
docker compose up -d
docker exec -it pg-primary psql -U postgres -c "SELECT client_addr, state FROM pg_stat_replication;"
docker exec -it pg-replica psql -U postgres -c "SELECT pg_is_in_recovery();"

Same two checks as step 7 — if the first returns a row with state = streaming and the second returns t, replication is live.

Ch.04

Command cheat sheet

Same 7 steps, compressed to one line each.

#WhereCommand / file
1PrimaryCREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD '…';
2Primarypg_hba.conf — add host replication replicator <replica_ip>/32 scram-sha-256
3Primarypostgresql.confwal_level=replica, max_wal_senders, max_replication_slots, restart
4PrimarySELECT pg_create_physical_replication_slot('replica_01_slot');
5Replicapg_basebackup -h <primary> -U replicator -D <datadir> -Fp -Xs -P -R --slot=replica_01_slot
6Replicastart Postgres — standby.signal puts it into standby mode automatically
7BothSELECT * FROM pg_stat_replication; (primary) / SELECT pg_is_in_recovery(); (replica)