Skip to content

Databases

Both environments run a primary + read replica setup with streaming replication.

RoleEnvironmentServerPortVolumeImage
PrimaryDEVAzure (4.180.181.86)5432dev_postgresql_datapostgres:16-alpine
ReplicaDEVAzure (4.180.181.86)dev_postgresql_replica_datapostgres:16-alpine
PrimaryUATContabo (84.247.134.135)5433uat_uat_postgresql_datapostgres:16-alpine
ReplicaUATContabo (84.247.134.135)uat_uat_postgresql_replica_datapostgres:16-alpine

Both DEV and UAT use physical streaming replication with a hot standby replica.

  • The primary streams WAL (Write-Ahead Log) records to the replica in real time
  • The replica continuously replays these records to stay in sync
  • The replica is read-only — it can serve read queries but cannot accept writes
  • Replication is asynchronous — the primary does not wait for the replica to confirm
  • A replication slot (replica_slot_1) ensures the primary retains WAL until the replica has consumed it

Both DEV and UAT primaries are tuned with:

ParameterValuePurpose
wal_levellogicalEnables logical replication (also needed for Debezium CDC)
shared_buffers4 GBMain memory cache
effective_cache_size8 GBPlanner hint for OS cache
work_mem16 MBPer-operation sort/hash memory
maintenance_work_mem512 MBFor VACUUM, CREATE INDEX
wal_buffers64 MBWAL write buffer
checkpoint_timeout10 minTime between checkpoints
max_wal_size2 GBMax WAL before forced checkpoint
min_wal_size512 MBMin WAL to retain
wal_compressionlz4Compress WAL to reduce I/O
checkpoint_completion_target0.9Spread checkpoint I/O
random_page_cost1.1Tuned for SSD

Both DEV and UAT replicas are tuned with:

ParameterValuePurpose
hot_standbyonAccept read queries on replica
hot_standby_feedbackonPrevent primary from vacuuming rows replica needs
max_standby_streaming_delay30sMax delay before canceling conflicting queries
shared_buffers4 GBSame as primary
effective_cache_size12 GBHigher — more read-focused
work_mem64 MB4x primary — heavier read queries
maintenance_work_mem256 MBLower — less write maintenance
random_page_cost1.0More aggressive index scan preference
max_parallel_workers_per_gather4Parallel query execution
max_parallel_workers8Total parallel workers
huge_pagestryUse OS huge pages if available
Terminal window
# On the primary — check replica is connected and streaming
docker exec -u postgres $(docker ps -q -f name=dev_postgresql -f status=running | head -1) \
psql -U sseris -c "SELECT client_addr, state, sent_lsn, replay_lsn FROM pg_stat_replication;"
# On the replica — confirm it's in recovery (read-only standby)
docker exec -u postgres $(docker ps -q -f name=dev_postgresql-replica -f status=running) \
psql -U sseris -c "SELECT pg_is_in_recovery();"
# Should return: t (true)

The primary must have this line in pg_hba.conf to allow replication connections:

host replication sseris all scram-sha-256

This is persisted in the primary’s volume. If the volume is ever recreated, this line must be added again and PostgreSQL reloaded:

Terminal window
docker exec $(docker ps -q -f name=dev_postgresql -f status=running | head -1) \
sh -c "echo 'host replication sseris all scram-sha-256' >> /var/lib/postgresql/data/pg_hba.conf"
docker exec -u postgres $(docker ps -q -f name=dev_postgresql -f status=running | head -1) \
pg_ctl reload -D /var/lib/postgresql/data

The database user is sseris (not postgres). The replica uses the same credentials as its primary.

Terminal window
# DEV primary
docker exec -it $(docker ps -q -f name=dev_postgresql -f status=running | head -1) psql -U sseris
# DEV replica (read-only)
docker exec -it $(docker ps -q -f name=dev_postgresql-replica -f status=running) psql -U sseris
# UAT primary
docker exec -it $(docker ps -q -f name=uat_postgresql -f status=running | head -1) psql -U sseris
# UAT replica (read-only)
docker exec -it $(docker ps -q -f name=uat_postgresql-replica -f status=running) psql -U sseris
Terminal window
# DEV (primary only — replica port not exposed)
psql -h 4.180.181.86 -p 5432 -U sseris
# UAT (primary only — replica port not exposed)
psql -h 84.247.134.135 -p 5433 -U sseris
\l
SELECT pg_database.datname, pg_size_pretty(pg_database_size(pg_database.datname))
FROM pg_database ORDER BY pg_database_size(pg_database.datname) DESC;
SELECT count(*) FROM pg_stat_activity;

PgBouncer sits in front of the primary PostgreSQL in both environments to manage connection pooling. Application services connect to PgBouncer (pgbouncer:6432), which manages connections to the actual database.

StorePurposeDEVUAT
RedisCaching, session storagedev_redis_datauat_uat_redis_data
KafkaEvent streamingdev_kafka_datauat_uat_kafka_data
RabbitMQMessage queuing (notifications)dev_rabbitmq_datauat_uat_rabbitmq_data
ImmuDBImmutable audit traildev_immudb_datauat_uat_immudb_data
Garage S3Object/file storagedev_garage_datauat_uat_garage_data