Deploy with Docker Compose

Deployment notes

Adjust all values to your own environment before you deploy, especially image addresses, data paths, ports, passwords, CIDR ranges, and resource limits.


Primary node preparation

Create the required directories

sudo su -
mkdir -p /data/workspace/install-postgres && cd /data/workspace/install-postgres
mkdir -p /data/workspace/install-postgres/{data,archive,scripts,config} && chown -R 999:999 /data/workspace/install-postgres && chmod -R 750 /data/workspace/install-postgres

Create the initialization script

cat > /data/workspace/install-postgres/scripts/init-primary.sh << 'EOF'
#!/bin/bash
set -e

until pg_isready -U postgres -d postgres; do sleep 2; done

psql -v ON_ERROR_STOP=1 --username "postgres" --dbname "postgres" <<-EOSQL
    CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'replicator_password';
    SELECT * FROM pg_create_physical_replication_slot('replication_slot_standby_1');
EOSQL

echo ">>> primary initialization completed"
EOF

chmod +x /data/workspace/install-postgres/scripts/init-primary.sh

Create pg_hba.conf

cat > /data/workspace/install-postgres/config/pg_hba.conf << 'EOF'
local   all             all                                     scram-sha-256
host    all             all             127.0.0.1/32            scram-sha-256
host    replication     replicator      10.14.0.0/16            scram-sha-256
host    all             pg_monitor      10.14.0.0/16            scram-sha-256
host    all             all             0.0.0.0/0               scram-sha-256
EOF

sudo chown 999:999 /data/workspace/install-postgres/config/pg_hba.conf
chmod 600 /data/workspace/install-postgres/config/pg_hba.conf

Primary Docker Compose

services:
  pg-primary:
    image: 10.14.0.37/postgres/postgres-18:V1
    container_name: pg-primary
    restart: unless-stopped
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: 'postgres@!QAZxsw2'
      POSTGRES_DB: postgres
      PGDATA: /var/lib/postgresql/data/pg18
    volumes:
      - /data/workspace/install-postgres/data:/var/lib/postgresql/data
      - /data/workspace/install-postgres/archive:/var/lib/postgresql/archive
      - /data/workspace/install-postgres/scripts/init-primary.sh:/docker-entrypoint-initdb.d/init-primary.sh
      - /data/workspace/install-postgres/config/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
    command: >
      postgres
      -c max_connections=5000
      -c shared_buffers=2GB
      -c effective_cache_size=6GB
      -c work_mem=8MB
      -c maintenance_work_mem=512MB
      -c wal_buffers=16MB
      -c checkpoint_timeout=10min
      -c max_wal_size=4GB
      -c wal_level=replica
      -c max_wal_senders=10
      -c max_replication_slots=10
      -c hot_standby=on
      -c archive_mode=on
      -c archive_command='test ! -f /var/lib/postgresql/archive/%f && cp %p /var/lib/postgresql/archive/%f'
      -c listen_addresses='*'
      -c synchronous_commit=off
      -c hba_file='/etc/postgresql/pg_hba.conf'
      -c password_encryption=scram-sha-256
      -c log_min_duration_statement=1000
      -c log_connections=on
      -c log_disconnections=on
      -c track_commit_timestamp=on
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 30s

Start the primary

docker compose up -d
sleep 30; docker logs pg-primary --tail 20
docker exec pg-primary pg_isready -U postgres -d postgres

Standby node preparation

Create the required directories

sudo su -
mkdir -p /data/workspace/install-postgres/data && cd /data/workspace/install-postgres
chown -R 999:999 /data/workspace/install-postgres && chmod -R 750 /data/workspace/install-postgres

Standby Docker Compose

services:
  pg-standby:
    image: 10.14.0.37/postgres/postgres-18:V1
    container_name: pg-standby
    restart: unless-stopped
    ports:
      - "5432:5432"
    environment:
      POSTGRES_PASSWORD: 'postgres@!QAZxsw2'
      TZ: Asia/Shanghai
      PGDATA: /var/lib/postgresql/data/pg18
    volumes:
      - /data/workspace/install-postgres/data:/var/lib/postgresql/data
    command: |
      bash -euc '
        DATA=/var/lib/postgresql/data/pg18
        echo ">>> waiting for primary 10.14.0.31..."
        until pg_isready -h 10.14.0.31 -p 5432 -U replicator; do sleep 2; done

        if [ -z "$$(ls -A $$DATA 2>/dev/null)" ]; then
          echo ">>> starting base backup..."
          export PGPASSWORD=replicator_password
          pg_basebackup \
            -h 10.14.0.31 \
            -p 5432 \
            -U replicator \
            -D "$$DATA" \
            -Fp -Xs -P -v -R \
            --slot=replication_slot_standby_1

          chown -R postgres:postgres "$$DATA"
          chmod 700 "$$DATA"
        fi

        exec docker-entrypoint.sh postgres \
          -c max_connections=5000 \
          -c shared_buffers=2GB \
          -c hot_standby=on \
          -c hot_standby_feedback=on \
          -c max_standby_streaming_delay=30s
      '
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 60s

Start the standby

docker compose up -d
docker ps
docker logs -f postgres-standby

Verify replication

# on the primary
docker exec -it pg-primary psql -U postgres -c "
SELECT
    client_addr,
    usename,
    state,
    sync_state,
    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) as lag
FROM pg_stat_replication;
"

# expected output
#  client_addr |   usename   |   state   | sync_state |   lag
# -------------+-------------+-----------+------------+---------
#  10.14.0.31  | replicator  | streaming | async      | 0 bytes

# test data sync
docker exec -it pg-primary psql -U postgres -c "
CREATE TABLE test_sync(id serial, data text);
INSERT INTO test_sync(data) VALUES('test primary-standby sync');
SELECT * FROM test_sync;
"

# on the standby
docker exec -it pg-standby psql -U postgres -c "SELECT * FROM test_sync;"

# verify read-only behavior
docker exec -it pg-standby psql -U postgres -c "INSERT INTO test_sync(data) VALUES('this should fail');"
# expected: cannot execute INSERT in a read-only transaction