This project demonstrates how to set up PostgreSQL Streaming Physical Replication using Docker containers. Streaming replication is one of several replication methods available in PostgreSQL, offering real-time data synchronization between a primary and one or more replica servers.
- Overview
- What is Streaming Physical Replication?
- Architecture
- Prerequisites
- Project Structure
- Setup and Configuration
- How It Works
- Testing the Replication
- Monitoring
- Other PostgreSQL Replication Methods
- Troubleshooting
- Cleanup
This setup creates a PostgreSQL primary-replica configuration where:
- The primary server handles all write operations
- The replica server receives real-time updates and serves read-only queries
- Data is replicated at the physical level (WAL records) for maximum consistency
Streaming Physical Replication is a method where:
- Physical Level: Replicates at the disk block level (not SQL statements)
- Streaming: WAL (Write-Ahead Log) records are streamed in real-time
- Asynchronous: By default, the primary doesn't wait for replica confirmation
- Hot Standby: Replicas can serve read-only queries while receiving updates
- ✅ Near real-time replication (typically milliseconds lag)
- ✅ Crash consistency - exact byte-for-byte copy
- ✅ High performance - minimal overhead on primary
- ✅ Automatic failover support with additional tools
- ✅ Read scaling - distribute read queries across replicas
- ❌ Entire database replication - can't replicate specific tables
- ❌ Same PostgreSQL version required on all nodes
- ❌ Read-only replicas - no writes allowed on standby
- ❌ Network dependent - requires stable network connection
┌─────────────────┐ WAL Stream ┌─────────────────┐
│ Primary │ ──────────────► │ Replica │
│ (Port 5433) │ │ (Port 5434) │
│ │ │ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ Writes │ │ │ │ Read-Only │ │
│ │ Reads │ │ │ │ Queries │ │
│ └─────────────┘ │ │ └─────────────┘ │
└─────────────────┘ └─────────────────┘
- Docker and Docker Compose installed
- At least 2GB RAM available
- Network connectivity between containers
pg-replication/
├── docker-compose.yml # Main orchestration file
├── primary/
│ ├── data/ # Primary database files (auto-generated)
│ └── init-primary.sh # Primary initialization script
├── replica/
│ ├── data/ # Replica database files (auto-generated)
│ └── init-replica.sh # Replica initialization script
└── README.md # This file
cd "Streaming Physical Replication/pg-replication"docker-compose up --build-
Primary Server:
- Initializes PostgreSQL database
- Configures WAL level to 'replica'
- Sets up replication slots
- Configures
pg_hba.conffor replication connections
-
Replica Server:
- Waits for primary to be ready
- Creates a replication slot on primary
- Performs base backup from primary using
pg_basebackup - Starts in standby mode
- Begins streaming WAL from primary
PostgreSQL uses WAL to ensure durability:
Application Write → WAL → Data Files → Replica Stream
The replication follows this sequence:
- Application writes data to Primary
- Primary writes WAL record
- Primary applies changes to data files
- WAL record is streamed to Replica
- Replica applies WAL record
- Applications can read from Replica
Primary Server (postgresql.conf):
wal_level = replica # Enable WAL for replication
max_wal_senders = 3 # Maximum concurrent connections
max_replication_slots = 3 # Maximum replication slots
wal_keep_size = 64MB # WAL retention sizeAuthentication (pg_hba.conf):
host replication postgres 0.0.0.0/0 md5
Replica Connection:
primary_conninfo = 'host=primary port=5432 user=postgres password=password'
primary_slot_name = 'replica1_slot'# Connect to primary and create test table
docker exec -it pg_primary psql -U postgres -c "
CREATE TABLE replication_test (
id SERIAL PRIMARY KEY,
data TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
"
# Insert test data
docker exec -it pg_primary psql -U postgres -c "
INSERT INTO replication_test (data) VALUES
('Hello from Primary Server'),
('This is a test record'),
('Real-time replication demo');
"# Check if data appears on replica (should be immediate)
docker exec -it pg_replica psql -U postgres -c "
SELECT * FROM replication_test ORDER BY id;
"# This should fail with "cannot execute INSERT in a read-only transaction"
docker exec -it pg_replica psql -U postgres -c "
INSERT INTO replication_test (data) VALUES ('This will fail');
"docker exec -it pg_primary psql -U postgres -c "
SELECT
client_addr,
application_name,
state,
sync_state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn
FROM pg_stat_replication;
"docker exec -it pg_replica psql -U postgres -c "
SELECT
pg_is_in_recovery() as is_replica,
pg_last_wal_receive_lsn() as receive_lsn,
pg_last_wal_replay_lsn() as replay_lsn,
pg_last_xact_replay_timestamp() as last_replay;
"docker exec -it pg_replica psql -U postgres -c "
SELECT
CASE
WHEN pg_last_wal_receive_lsn() = pg_last_wal_replay_lsn()
THEN 0
ELSE EXTRACT (EPOCH FROM now() - pg_last_xact_replay_timestamp())
END AS lag_seconds;
"┌─────────────┐ SQL Commands ┌─────────────┐
│ Primary │ ──────────────► │ Replica │
│ │ │ │
│ All Tables │ │ Select │
│ │ │ Tables │
└─────────────┘ └─────────────┘
Characteristics:
- Replicates logical changes (SQL statements)
- Can replicate specific tables/databases
- Allows different PostgreSQL versions
- Supports bi-directional replication
- Higher overhead than physical replication
Use Cases:
- Selective table replication
- Cross-version replication
- Data transformation during replication
┌─────────────┐ WAL + ACK ┌─────────────┐
│ Primary │ ◄──────────────► │ Replica │
│ │ (Confirmed) │ │
└─────────────┘ └─────────────┘
Configuration:
synchronous_standby_names = 'replica1'
synchronous_commit = onCharacteristics:
- Primary waits for replica confirmation
- Zero data loss guarantee
- Higher latency for write operations
- Requires very stable network
┌─────────┐ WAL ┌─────────┐ WAL ┌─────────┐
│ Primary │────►│Replica1 │────►│Replica2 │
└─────────┘ └─────────┘ └─────────┘
Use Cases:
- Reducing network load on primary
- Geographic distribution
- Backup hierarchies
┌─────────┐ WAL Archive ┌─────────────┐
│ Primary │────────────►│ Archive │
└─────────┘ │ Storage │
└─────────────┘
Characteristics:
- Continuous archiving of WAL files
- Restore to any point in time
- Primarily for backup/recovery
- Not real-time like streaming
| Method | Real-time | Granularity | Cross-version | Data Loss Risk | Complexity |
|---|---|---|---|---|---|
| Streaming Physical | ✅ Yes | Database | ❌ No | Low (async) | Low |
| Synchronous Physical | ✅ Yes | Database | ❌ No | None | Medium |
| Logical | ✅ Yes | Table | ✅ Yes | Low | Medium |
| Cascading | ✅ Yes | Database | ❌ No | Low | High |
| PITR | ❌ No | Database | ❌ No | Variable | Medium |
Error: could not translate host name "primary" to address
Solution: Ensure containers are on the same Docker network
FATAL: password authentication failed for user "postgres"
Solution: Check pg_hba.conf configuration and password settings
FATAL: no pg_hba.conf entry for replication connection
Solution: Add replication entry to pg_hba.conf:
docker exec pg_primary bash -c "echo 'host replication postgres 0.0.0.0/0 md5' >> /var/lib/postgresql/data/pg_hba.conf"
docker exec pg_primary psql -U postgres -c "SELECT pg_reload_conf();""root" execution of the PostgreSQL server is not permitted
Solution: Ensure PostgreSQL runs as postgres user, not root
FATAL: could not receive data from WAL stream
Solution: Increase wal_keep_size or use replication slots
# Check container status
docker ps -a | grep pg_
# View logs
docker logs pg_primary
docker logs pg_replica
# Check PostgreSQL processes
docker exec pg_primary ps aux | grep postgres
# Test connectivity
docker exec pg_replica pg_isready -h primary -p 5432 -U postgres# postgresql.conf
wal_sender_timeout = 60s
wal_receiver_timeout = 60s
max_wal_size = 1GBshared_buffers = 256MB # 1/4 of available RAM
effective_cache_size = 1GB # Available system cache
maintenance_work_mem = 64MB # Memory for maintenance operationscheckpoint_completion_target = 0.9
checkpoint_timeout = 15min
max_wal_size = 2GB- Use SSL for replication connections
- Restrict access in
pg_hba.conf - Use VPN for cross-datacenter replication
# Use SCRAM-SHA-256 instead of MD5
host replication postgres 0.0.0.0/0 scram-sha-256ssl = on
ssl_cert_file = 'server.crt'
ssl_key_file = 'server.key'
ssl_ca_file = 'ca.crt'# On replica, promote to primary
docker exec pg_replica pg_ctl promote -D /var/lib/postgresql/data
# Update application connection strings
# Restart old primary as new replica# In docker-compose.yml
replica2:
image: postgres:15
container_name: pg_replica2
# ... similar configuration# Backup from replica to reduce primary load
docker exec pg_replica pg_dump -U postgres postgres > backup.sql# Stop and remove containers
docker-compose down -v
# Remove data directories
sudo rm -rf primary/data/* replica/data/*
# Remove Docker images (optional)
docker rmi postgres:15Streaming Physical Replication is an excellent choice for:
- ✅ High availability setups
- ✅ Read scaling requirements
- ✅ Disaster recovery scenarios
- ✅ Real-time reporting needs
This method provides a robust, performant, and relatively simple solution for PostgreSQL replication. While it has some limitations (like requiring identical PostgreSQL versions), its benefits make it the most popular replication method for production PostgreSQL deployments.
The setup demonstrated here provides a solid foundation that can be extended with additional features like monitoring, automated failover, and multiple replicas as your requirements grow.