Configure PostgreSQL 17 with streaming replication for high availability, then add PgBouncer connection pooling with intelligent load balancing across primary and replica servers for production-grade database infrastructure.
Prerequisites
- At least 3 servers (1 primary, 2+ replicas)
- Root or sudo access
- Network connectivity between servers
- Basic PostgreSQL knowledge
What this solves
PostgreSQL streaming replication creates real-time copies of your primary database on replica servers, providing automatic failover and read scaling. PgBouncer adds connection pooling and load balancing, distributing read queries to replicas while routing writes to the primary. This setup handles database high availability, reduces connection overhead, and scales read performance across multiple servers.
Step-by-step configuration
Install PostgreSQL 17 on all servers
Install PostgreSQL 17 on your primary server and all replica servers. You'll need at least two servers for this setup.
sudo apt update
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list
sudo apt update
sudo apt install -y postgresql-17 postgresql-client-17 postgresql-contrib-17
Initialize PostgreSQL on primary server
Initialize the PostgreSQL database cluster on your primary server and start the service.
sudo systemctl enable postgresql
sudo systemctl start postgresql
sudo systemctl status postgresql
Configure primary server for replication
Edit the PostgreSQL configuration to enable streaming replication. This allows replica servers to connect and receive real-time database changes.
listen_addresses = '*'
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
wal_keep_size = 1GB
archive_mode = on
archive_command = '/bin/true'
checkpoint_completion_target = 0.9
wal_compression = on
Configure authentication for replication
Set up PostgreSQL host-based authentication to allow replica servers to connect for replication.
# Add these lines at the end, replace with your replica server IPs
host replication replicator 203.0.113.11/32 md5
host replication replicator 203.0.113.12/32 md5
host all pgbouncer 203.0.113.10/32 md5
host all pgbouncer 203.0.113.11/32 md5
host all pgbouncer 203.0.113.12/32 md5
Create replication user
Create a dedicated PostgreSQL user for replication and a user for PgBouncer connections.
sudo -u postgres psql -c "CREATE USER replicator REPLICATION LOGIN ENCRYPTED PASSWORD 'SecureReplPassword123!';"
sudo -u postgres psql -c "CREATE USER pgbouncer LOGIN ENCRYPTED PASSWORD 'SecurePgBouncerPass123!';"
Restart primary PostgreSQL
Restart PostgreSQL on the primary server to apply the replication configuration changes.
sudo systemctl restart postgresql
Create base backup for replica servers
Use pg_basebackup to create an initial copy of the primary database for each replica server. Run this on each replica server.
sudo systemctl stop postgresql
sudo rm -rf /var/lib/postgresql/17/main/*
sudo -u postgres pg_basebackup -h 203.0.113.10 -D /var/lib/postgresql/17/main -U replicator -P -v -R -W -C -S replica_slot_1
Configure replica servers
Edit the PostgreSQL configuration on replica servers to optimize for read-only workloads and enable hot standby.
listen_addresses = '*'
hot_standby = on
max_standby_archive_delay = 30s
max_standby_streaming_delay = 30s
wal_receiver_status_interval = 2s
hot_standby_feedback = on
Start replica servers
Start PostgreSQL on all replica servers. They will automatically begin streaming replication from the primary.
sudo systemctl start postgresql
sudo systemctl enable postgresql
Install PgBouncer on connection pooling server
Install PgBouncer on a dedicated server that will handle connection pooling and load balancing between your PostgreSQL servers.
sudo apt update
sudo apt install -y pgbouncer
Configure PgBouncer main settings
Configure PgBouncer with connection pooling settings and define your PostgreSQL servers for load balancing.
[databases]
; Primary server for writes
myapp_write = host=203.0.113.10 port=5432 dbname=myapp
; Replica servers for reads
myapp_read = host=203.0.113.11 port=5432 dbname=myapp
myapp_read_2 = host=203.0.113.12 port=5432 dbname=myapp
[pgbouncer]
listen_addr = *
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
logfile = /var/log/pgbouncer/pgbouncer.log
pidfile = /var/run/pgbouncer/pgbouncer.pid
admin_users = pgbouncer
stats_users = pgbouncer
pool_mode = transaction
server_reset_query = DISCARD ALL
max_client_conn = 1000
default_pool_size = 100
server_lifetime = 3600
server_idle_timeout = 600
client_idle_timeout = 600
server_connect_timeout = 15
query_timeout = 0
Configure PgBouncer authentication
Create the user authentication file for PgBouncer with encrypted passwords.
echo '"pgbouncer" "md5' $(echo -n 'SecurePgBouncerPass123!pgbouncer' | md5sum | cut -d' ' -f1) '"' | sudo tee /etc/pgbouncer/userlist.txt
echo '"myapp_user" "md5' $(echo -n 'MyAppPassword123!myapp_user' | md5sum | cut -d' ' -f1) '"' | sudo tee -a /etc/pgbouncer/userlist.txt
Set PgBouncer file permissions
Set secure permissions on PgBouncer configuration files to protect database credentials.
sudo chown pgbouncer:pgbouncer /etc/pgbouncer/pgbouncer.ini /etc/pgbouncer/userlist.txt
sudo chmod 640 /etc/pgbouncer/pgbouncer.ini /etc/pgbouncer/userlist.txt
sudo mkdir -p /var/log/pgbouncer
sudo chown pgbouncer:pgbouncer /var/log/pgbouncer
Create application database and user
Create your application database and user on the primary PostgreSQL server. This will automatically replicate to all replica servers.
sudo -u postgres createdb myapp
sudo -u postgres psql -c "CREATE USER myapp_user WITH ENCRYPTED PASSWORD 'MyAppPassword123!';"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE myapp TO myapp_user;"
Configure systemd for PgBouncer
Create a systemd service file for PgBouncer to ensure it starts automatically and runs with proper user permissions.
[Unit]
Description=PgBouncer PostgreSQL connection pooler
Documentation=https://pgbouncer.github.io/
After=network.target
[Service]
Type=notify
User=pgbouncer
Group=pgbouncer
ExecStart=/usr/bin/pgbouncer /etc/pgbouncer/pgbouncer.ini
ExecReload=/bin/kill -HUP $MAINPID
KillMode=mixed
KillSignal=SIGINT
TimeoutSec=120
[Install]
WantedBy=multi-user.target
Start and enable PgBouncer
Start PgBouncer and enable it to start automatically on boot.
sudo systemctl daemon-reload
sudo systemctl enable pgbouncer
sudo systemctl start pgbouncer
sudo systemctl status pgbouncer
Configure load balancing script
Create a simple script to implement read/write splitting by connecting to different PgBouncer databases based on query type.
#!/bin/bash
Simple PostgreSQL load balancing script
Usage: pg_loadbalancer.sh [read|write] "SQL_COMMAND"
MODE=$1
QUERY=$2
PGBOUNCER_HOST="203.0.113.13" # Replace with your PgBouncer server IP
PGBOUNCER_PORT="6432"
USERNAME="myapp_user"
PASSWORD="MyAppPassword123!"
if [ "$MODE" = "write" ]; then
DATABASE="myapp_write"
else
# Round-robin between read replicas
REPLICA_NUM=$((RANDOM % 2 + 1))
if [ $REPLICA_NUM -eq 1 ]; then
DATABASE="myapp_read"
else
DATABASE="myapp_read_2"
fi
fi
PGPASSWORD="$PASSWORD" psql -h "$PGBOUNCER_HOST" -p "$PGBOUNCER_PORT" -U "$USERNAME" -d "$DATABASE" -c "$QUERY"
sudo chmod +x /usr/local/bin/pg_loadbalancer.sh
Configure connection monitoring
Set up a monitoring script to check the health of your PostgreSQL servers and automatically adjust PgBouncer configuration if servers become unavailable.
#!/bin/bash
PostgreSQL cluster health monitoring
PRIMARY_HOST="203.0.113.10"
REPLICA1_HOST="203.0.113.11"
REPLICA2_HOST="203.0.113.12"
PGBOUNCER_HOST="203.0.113.13"
MONITOR_USER="pgbouncer"
MONITOR_PASS="SecurePgBouncerPass123!"
check_server() {
local host=$1
PGPASSWORD="$MONITOR_PASS" psql -h "$host" -U "$MONITOR_USER" -d postgres -c "SELECT 1;" &>/dev/null
return $?
}
Check primary server
if check_server "$PRIMARY_HOST"; then
echo "$(date): Primary server ($PRIMARY_HOST) is healthy"
else
echo "$(date): WARNING: Primary server ($PRIMARY_HOST) is down!"
# Log to syslog for alerting
logger -t pg_monitor "CRITICAL: PostgreSQL primary server is down"
fi
Check replica servers
for replica in "$REPLICA1_HOST" "$REPLICA2_HOST"; do
if check_server "$replica"; then
echo "$(date): Replica server ($replica) is healthy"
else
echo "$(date): WARNING: Replica server ($replica) is down!"
logger -t pg_monitor "WARNING: PostgreSQL replica server $replica is down"
fi
done
sudo chmod +x /usr/local/bin/pg_health_monitor.sh
Schedule health monitoring
Add the health monitoring script to cron to run every 5 minutes and log results.
sudo crontab -e
Add this line to the crontab:
/5 * /usr/local/bin/pg_health_monitor.sh >> /var/log/postgresql/health_monitor.log 2>&1
Verify your setup
Test the streaming replication and PgBouncer connection pooling to ensure everything is working correctly.
# Check replication status on primary server
sudo -u postgres psql -c "SELECT client_addr, state, sync_state FROM pg_stat_replication;"
Check replication lag on replica servers
sudo -u postgres psql -c "SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) AS lag_seconds;"
Test PgBouncer connections
PGPASSWORD="MyAppPassword123!" psql -h 203.0.113.13 -p 6432 -U myapp_user -d myapp_write -c "SELECT version();"
PGPASSWORD="MyAppPassword123!" psql -h 203.0.113.13 -p 6432 -U myapp_user -d myapp_read -c "SELECT version();"
Check PgBouncer statistics
PGPASSWORD="SecurePgBouncerPass123!" psql -h 203.0.113.13 -p 6432 -U pgbouncer -d pgbouncer -c "SHOW POOLS;"
PGPASSWORD="SecurePgBouncerPass123!" psql -h 203.0.113.13 -p 6432 -U pgbouncer -d pgbouncer -c "SHOW STATS;"
Test load balancing script
/usr/local/bin/pg_loadbalancer.sh write "CREATE TABLE test_table (id SERIAL PRIMARY KEY, name VARCHAR(100));"
/usr/local/bin/pg_loadbalancer.sh read "SELECT * FROM test_table;"
Run health monitor manually
/usr/local/bin/pg_health_monitor.sh
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Replica can't connect to primary | pg_hba.conf missing replication entry | Add host replication replicator IP/32 md5 to primary |
| PgBouncer authentication fails | User not in userlist.txt or wrong password hash | Recreate userlist.txt with correct MD5 hash |
| Connection refused to PgBouncer | Firewall blocking port 6432 | sudo ufw allow 6432 or configure iptables |
| High replication lag | Network issues or heavy write load | Check wal_keep_size and network bandwidth |
| PgBouncer shows no servers | PostgreSQL servers unreachable | Check PostgreSQL status and network connectivity |
| Replica shows as async when expecting sync | synchronous_standby_names not configured | Set synchronous_standby_names = 'replica_slot_1' on primary |
Next steps
- Monitor PostgreSQL performance with Prometheus and Grafana dashboards
- Configure PostgreSQL 17 streaming replication for high availability with automatic failover
- Set up automated PostgreSQL backup with pgBackRest and point-in-time recovery
- Configure PostgreSQL SSL encryption and advanced security hardening
- Set up PostgreSQL connection pooling optimization for Kubernetes workloads
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# PostgreSQL 17 Streaming Replication with PgBouncer Setup Script
# Production-ready installation and configuration
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m'
# Configuration variables
readonly SCRIPT_NAME=$(basename "$0")
readonly TOTAL_STEPS=12
# Function definitions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
cleanup_on_error() {
log_error "Installation failed. Cleaning up..."
systemctl stop postgresql* 2>/dev/null || true
systemctl stop pgbouncer 2>/dev/null || true
}
trap cleanup_on_error ERR
show_usage() {
cat << EOF
Usage: $SCRIPT_NAME <role> [options]
Roles:
primary <primary_ip> - Configure as primary server
replica <primary_ip> <slot_name> - Configure as replica server
pgbouncer <primary_ip> <replica_ip1,replica_ip2,...> - Configure PgBouncer
Examples:
$SCRIPT_NAME primary 203.0.113.10
$SCRIPT_NAME replica 203.0.113.10 replica_slot_1
$SCRIPT_NAME pgbouncer 203.0.113.10 203.0.113.11,203.0.113.12
EOF
}
# Validate arguments
if [[ $# -lt 2 ]]; then
show_usage
exit 1
fi
readonly ROLE="$1"
readonly PRIMARY_IP="$2"
readonly SLOT_NAME="${3:-replica_slot_1}"
readonly REPLICA_IPS="${3:-}"
# Detect OS and package manager
if [[ ! -f /etc/os-release ]]; then
log_error "/etc/os-release not found. Cannot detect distribution."
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
PG_SERVICE="postgresql"
PG_VERSION="17"
PG_DATA_DIR="/var/lib/postgresql/17/main"
PG_CONFIG_DIR="/etc/postgresql/17/main"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
PG_SERVICE="postgresql-17"
PG_VERSION="17"
PG_DATA_DIR="/var/lib/pgsql/17/data"
PG_CONFIG_DIR="/var/lib/pgsql/17/data"
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
PG_SERVICE="postgresql"
PG_VERSION="17"
PG_DATA_DIR="/var/lib/pgsql/data"
PG_CONFIG_DIR="/var/lib/pgsql/data"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
PG_SERVICE="postgresql"
PG_VERSION="17"
PG_DATA_DIR="/var/lib/pgsql/data"
PG_CONFIG_DIR="/var/lib/pgsql/data"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
# Check prerequisites
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root"
exit 1
fi
# Step 1: Update system packages
echo "[1/$TOTAL_STEPS] Updating system packages..."
$PKG_UPDATE
# Step 2: Install PostgreSQL 17
echo "[2/$TOTAL_STEPS] Installing PostgreSQL 17..."
if [[ "$ID" == "ubuntu" || "$ID" == "debian" ]]; then
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list
apt update
$PKG_INSTALL postgresql-17 postgresql-client-17 postgresql-contrib-17
else
$PKG_INSTALL https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
$PKG_INSTALL postgresql17-server postgresql17-contrib postgresql17
fi
# Step 3: Initialize PostgreSQL (if needed)
echo "[3/$TOTAL_STEPS] Initializing PostgreSQL..."
if [[ "$ID" == "ubuntu" || "$ID" == "debian" ]]; then
systemctl enable postgresql
if [[ "$ROLE" == "primary" ]]; then
systemctl start postgresql
fi
else
if [[ "$ROLE" == "primary" ]]; then
/usr/pgsql-17/bin/postgresql-17-setup initdb
systemctl enable postgresql-17
systemctl start postgresql-17
else
systemctl enable postgresql-17
fi
fi
# Configure based on role
case "$ROLE" in
"primary")
echo "[4/$TOTAL_STEPS] Configuring primary server..."
# Backup original config
cp "$PG_CONFIG_DIR/postgresql.conf" "$PG_CONFIG_DIR/postgresql.conf.backup"
# Configure postgresql.conf
cat >> "$PG_CONFIG_DIR/postgresql.conf" << 'EOF'
# Replication settings
listen_addresses = '*'
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
wal_keep_size = 1024
archive_mode = on
archive_command = '/bin/true'
checkpoint_completion_target = 0.9
wal_compression = on
EOF
echo "[5/$TOTAL_STEPS] Configuring authentication..."
cat >> "$PG_CONFIG_DIR/pg_hba.conf" << EOF
# Replication connections
host replication replicator 0.0.0.0/0 md5
host all pgbouncer 0.0.0.0/0 md5
EOF
echo "[6/$TOTAL_STEPS] Creating replication users..."
systemctl restart $PG_SERVICE
sleep 5
sudo -u postgres psql -c "CREATE USER replicator REPLICATION LOGIN ENCRYPTED PASSWORD 'SecureReplPassword123!';" || true
sudo -u postgres psql -c "CREATE USER pgbouncer LOGIN ENCRYPTED PASSWORD 'SecurePgBouncerPass123!';" || true
log_info "Primary server configured successfully"
;;
"replica")
echo "[4/$TOTAL_STEPS] Configuring replica server..."
systemctl stop $PG_SERVICE 2>/dev/null || true
rm -rf "$PG_DATA_DIR"/*
echo "[5/$TOTAL_STEPS] Creating base backup..."
sudo -u postgres PGPASSWORD='SecureReplPassword123!' pg_basebackup -h "$PRIMARY_IP" -D "$PG_DATA_DIR" -U replicator -P -v -R -W -C -S "$SLOT_NAME"
echo "[6/$TOTAL_STEPS] Configuring replica settings..."
cat >> "$PG_CONFIG_DIR/postgresql.conf" << 'EOF'
# Hot standby settings
listen_addresses = '*'
hot_standby = on
max_standby_archive_delay = 30s
max_standby_streaming_delay = 30s
wal_receiver_status_interval = 2s
hot_standby_feedback = on
EOF
chown -R postgres:postgres "$PG_DATA_DIR"
chmod 750 "$PG_DATA_DIR"
systemctl start $PG_SERVICE
log_info "Replica server configured successfully"
;;
"pgbouncer")
echo "[4/$TOTAL_STEPS] Installing PgBouncer..."
$PKG_INSTALL pgbouncer
echo "[5/$TOTAL_STEPS] Configuring PgBouncer..."
cat > /etc/pgbouncer/pgbouncer.ini << EOF
[databases]
* = host=$PRIMARY_IP port=5432
[pgbouncer]
listen_addr = *
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = session
max_client_conn = 1000
default_pool_size = 25
reserve_pool_size = 5
log_connections = 1
log_disconnections = 1
EOF
echo "[6/$TOTAL_STEPS] Creating user authentication..."
echo '"pgbouncer" "md5$(echo -n SecurePgBouncerPass123!pgbouncer | md5sum | cut -d\ -f1)"' > /etc/pgbouncer/userlist.txt
chown pgbouncer:pgbouncer /etc/pgbouncer/userlist.txt
chmod 640 /etc/pgbouncer/userlist.txt
systemctl enable pgbouncer
systemctl start pgbouncer
log_info "PgBouncer configured successfully"
;;
*)
log_error "Invalid role: $ROLE"
show_usage
exit 1
;;
esac
# Configure firewall
echo "[7/$TOTAL_STEPS] Configuring firewall..."
if command -v firewall-cmd &> /dev/null; then
firewall-cmd --permanent --add-port=5432/tcp
if [[ "$ROLE" == "pgbouncer" ]]; then
firewall-cmd --permanent --add-port=6432/tcp
fi
firewall-cmd --reload
elif command -v ufw &> /dev/null; then
ufw allow 5432/tcp
if [[ "$ROLE" == "pgbouncer" ]]; then
ufw allow 6432/tcp
fi
fi
# Final verification
echo "[8/$TOTAL_STEPS] Verifying installation..."
if [[ "$ROLE" == "pgbouncer" ]]; then
systemctl is-active --quiet pgbouncer && log_info "PgBouncer is running" || log_error "PgBouncer is not running"
else
systemctl is-active --quiet $PG_SERVICE && log_info "PostgreSQL is running" || log_error "PostgreSQL is not running"
fi
echo -e "\n${GREEN}Installation completed successfully!${NC}"
echo "Role: $ROLE"
[[ "$ROLE" != "primary" ]] && echo "Primary IP: $PRIMARY_IP"
[[ "$ROLE" == "replica" ]] && echo "Slot name: $SLOT_NAME"
log_warn "Remember to:"
log_warn "- Change default passwords in production"
log_warn "- Restrict IP access in pg_hba.conf"
log_warn "- Configure SSL certificates for secure connections"
log_warn "- Set up monitoring and backup procedures"
Review the script before running. Execute with: bash install.sh