PostgreSQL in Docker: Production Deployment Guide

Deploy PostgreSQL containers with Docker for persistent database storage. Learn configuration management, backup strategies, high availability, and security hardening for production PostgreSQL deployments.

PostgreSQL Docker container architecture showing persistent storage and replication

Introduction

PostgreSQL containers provide flexible database deployment while ensuring consistent environments across development and production. Docker containerization simplifies PostgreSQL installation, configuration management, and scaling. This guide covers production-ready PostgreSQL container deployment with persistent storage, backup strategies, and security configuration.

Database containers require careful consideration of data persistence, performance tuning, and high availability. Unlike stateless applications, database containers must preserve data across restarts while maintaining performance characteristics appropriate for production workloads.

PostgreSQL’s robust feature set complements container deployment patterns. Streaming replication, point-in-time recovery, and extensive extension support make PostgreSQL suitable for demanding applications. Container orchestration platforms enhance these capabilities with automated failover and horizontal scaling.

Basic PostgreSQL Container Deployment

Single-instance PostgreSQL containers suit development environments and applications without high availability requirements.

Running PostgreSQL Container

The official PostgreSQL image provides robust functionality with minimal configuration:

# Run basic PostgreSQL container
docker run -d \
  --name postgres-server \
  -e POSTGRES_PASSWORD=secure-password \
  -e POSTGRES_USER=appuser \
  -e POSTGRES_DB=appdb \
  -v postgres-data:/var/lib/postgresql/data \
  -p 5432:5432 \
  postgres:15

# Verify container is running
docker ps | grep postgres

# Test connection
docker exec postgres-server psql -U appuser -d appdb -c "SELECT version();"

Volume mounts preserve database data across container lifecycle. Docker manages volume lifecycle separately from containers, ensuring data persistence during updates and restarts.

Connection Configuration

Configure client connections for remote access:

# Run with custom configuration
docker run -d \
  --name postgres-configured \
  -e POSTGRES_PASSWORD=secure-password \
  -e POSTGRES_USER=appuser \
  -e POSTGRES_DB=appdb \
  -v postgres-data:/var/lib/postgresql/data \
  -v $(pwd)/postgresql.conf:/etc/postgresql/postgresql.conf:ro \
  -v $(pwd)/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro \
  -p 5432:5432 \
  postgres:15 \
  -c 'config_file=/etc/postgresql/postgresql.conf'

Custom configuration files enable tuning appropriate for production workloads. The -c flag passes configuration parameters to the PostgreSQL process.

Environment Variables Reference

PostgreSQL image uses specific environment variables:

# Essential variables
POSTGRES_PASSWORD      # Required: superuser password
POSTGRES_USER          # Default: postgres
POSTGRES_DB           # Default: same as POSTGRES_USER

# Optional initialization scripts
POSTGRES_INITDB_ARGS   # Arguments to initdb command
POSTGRES_INITDB_WAL_DIR  # WAL directory location

Initialization scripts in /docker-entrypoint-initdb.d/ execute during first container startup, enabling database schema creation and initial data population.

Configuration Management

Custom PostgreSQL configurations optimize performance and enable security features.

Performance Tuning

Create custom postgresql.conf for production workloads:

# postgresql.conf
listen_addresses = '*'

# Memory configuration
shared_buffers = 4GB
effective_cache_size = 12GB
work_mem = 256MB
maintenance_work_mem = 1GB

# Write performance
wal_buffers = 64MB
checkpoint_completion_target = 0.9
max_wal_size = 4GB
min_wal_size = 1GB

# Connection configuration
max_connections = 200
tcp_keepalives_idle = 60
tcp_keepalives_interval = 10
tcp_keepalives_count = 5

# Query optimization
random_page_cost = 1.1
effective_io_concurrency = 200

# Logging
log_statement = 'ddl'
log_min_duration_statement = 1000
log_connections = on
log_disconnections = on

# Security
password_encryption = scram-sha-256

Apply configuration through volume mounts or command-line parameters.

Authentication Configuration

Configure pg_hba.conf for appropriate authentication:

# pg_hba.conf
# TYPE  DATABASE        USER            ADDRESS                 METHOD

# Local connections
local   all             all                                     trust

# IPv4 local network
host    all             all             127.0.0.1/32            scram-sha-256

# IPv6 local network
host    all             all             ::1/128                 scram-sha-256

# Application network
host    appdb           appuser         10.0.0.0/8              scram-sha-256
host    all             admin           192.168.0.0/16         scram-sha-256

# Replication connections
host    replication     replicator      10.0.0.0/8              scram-sha-256

Authentication methods balance security with operational requirements. scram-sha-256 provides strong password authentication. Certificate-based authentication suits high-security environments.

Docker Compose Configuration

Define complete PostgreSQL deployment with Docker Compose:

# docker-compose.postgres.yml
version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    container_name: postgres-server
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_USER: appuser
      POSTGRES_DB: appdb
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./postgresql.conf:/etc/postgresql/postgresql.conf:ro
      - ./pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
      - ./init-scripts:/docker-entrypoint-initdb.d/:ro
    ports:
      - "5432:5432"
    command: >
      postgres
      -c 'config_file=/etc/postgresql/postgresql.conf'
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          memory: 8G
        reservations:
          memory: 4G

volumes:
  postgres-data:

Health checks enable orchestration platform integration for reliable service discovery.

Backup and Recovery

Database backups protect against data loss from hardware failure, human error, and corruption.

Continuous Archiving with WAL

Configure continuous archiving for point-in-time recovery:

# postgresql.conf WAL configuration
wal_level = replica
archive_mode = on
archive_command = '/usr/bin/wal-g archive-push %p -d /var/lib/wal-g'

# Include WAL files in base backup
full_page_writes = on
wal_log_hints = on

WAL archiving enables recovery to any point since the last base backup, minimizing potential data loss.

Base Backup Procedures

Create regular base backups for disaster recovery:

# Create base backup using pg_basebackup
docker exec postgres-server \
  pg_basebackup -D /var/lib/postgresql/backups/base \
  -U replication -Fp -Xs -P -R

# Compress and archive backup
tar -czvf pg-backup-$(date +%Y%m%d).tar.gz -C /backups base/

Schedule base backups according to recovery time objectives. Weekly base backups combined with continuous WAL archiving enable point-in-time recovery with minimal data loss.

Point-in-Time Recovery

Restore to specific points in time using archived WAL files:

# Stop PostgreSQL
docker stop postgres-server

# Move corrupted data directory
mv postgres-data postgres-data-corrupt

# Create new data directory
docker run --rm -v postgres-data:/var/lib/postgresql/data \
  -v postgres-backups:/backups:ro \
  postgres:15-alpine \
  mkdir -p /var/lib/postgresql/data

# Create recovery.conf
cat > postgres-data/recovery.conf <<EOF
restore_command = 'cp /backups/wal/%f %p'
recovery_target_time = '2026-02-08 14:00:00 UTC'
recovery_target_action = 'promote'
EOF

# Start PostgreSQL
docker start postgres-server

Point-in-time recovery requires WAL archives containing the target recovery point. Test recovery procedures regularly to verify backup integrity.

Docker-Specific Backup Strategies

Containerized backups use volume mounts and temporary containers:

# Simple pg_dump backup
docker exec postgres-server \
  pg_dump -U appuser appdb > backup-$(date +%Y%m%d).sql

# Compressed backup
docker exec postgres-server \
  pg_dump -U appuser appdb | gzip > backup-$(date +%Y%m%d).sql.gz

# Backup with custom format
docker exec postgres-server \
  pg_dump -U appuser -F c appdb > backup-$(date +%Y%m%d).custom

# Scheduled backup script
#!/bin/bash
BACKUP_DIR="/backups/postgres"
DATE=$(date +%Y%m%d-%H%M%S)

docker exec postgres-server \
  pg_dump -U appuser appdb | gzip > $BACKUP_DIR/$DATE.sql.gz

# Keep last 30 days
find $BACKUP_DIR -name "*.gz" -mtime +30 -delete

pg_dump creates logical backups portable across PostgreSQL versions. Physical backups with pg_basebackup enable faster recovery but require matching versions.

High Availability

PostgreSQL streaming replication provides real-time data synchronization between primary and replica servers.

Setting Up Streaming Replication

Configure primary for replication:

# Primary postgresql.conf
wal_level = replica
max_wal_senders = 5
max_replication_slots = 5
wal_keep_size = 1GB

# Create replication user
docker exec postgres-server psql -U postgres -c \
  "CREATE USER replicator WITH REPLICATION PASSWORD 'secure-password';"

Configure replica for streaming:

# Replica postgresql.conf
hot_standby = on

# Create recovery.conf for replication
cat > /var/lib/postgresql/data/recovery.conf <<EOF
primary_conninfo = 'host=postgres-primary user=replicator password=secure-password'
standby_mode = on
trigger_file = '/var/lib/postgresql/data/failover_trigger'
EOF

# Clone primary using pg_basebackup
docker run --rm \
  -v postgres-replica:/var/lib/postgresql/data \
  -v postgres-primary-data:/source:ro \
  postgres:15-alpine \
  sh -c "pg_basebackup -h postgres-primary -U replicator -D /var/lib/postgresql/data -Fp -Xs -P -R"

Streaming replication provides real-time data synchronization. Replicas accept read queries, distributing load across primary and replicas.

Automatic Failover with Patroni

Patroni provides automated failover and configuration management for PostgreSQL clusters:

# docker-compose.patroni.yml
version: '3.8'

services:
  patroni:
    image: patroni:latest
    container_name: patroni-node
    environment:
      PATRONI_NAME: ${HOSTNAME}
      PATRONI_SCOPE: postgres-cluster
      PATRONI_RESTAPI_ADDRESS: 0.0.0.0:8008
      PATRONI_POSTGRESQL_DATA_DIR: /data/postgres
      PATRONI_POSTGRESQL_PGPASS: /tmp/pgpass
      PATRONI_REPLICATION_USERNAME: replicator
      PATRONI_REPLICATION_PASSWORD: ${REPLICATION_PASSWORD}
      PATRONI_SUPERUSER_USERNAME: postgres
      PATRONI_SUPERUSER_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - patroni-data:/data
      - postgres-wal:/wal
    command: >
      patroni /data/postgres
    depends_on:
      - etcd

  etcd:
    image: etcd:latest
    container_name: etcd
    environment:
      ETCTL_DATA_DIR: /etcd
    volumes:
      - etcd-data:/etcd

volumes:
  patroni-data:
  postgres-wal:
  etcd-data:

Patroni manages leader election, failover, and configuration synchronization. Consul, ZooKeeper, or Kubernetes can replace etcd for coordination.

Security Configuration

Database security requires multiple layers of protection.

Network Security

Restrict PostgreSQL network access:

# Run PostgreSQL on internal network only
docker network create postgres-network

docker run -d \
  --name postgres-internal \
  --network postgres-network \
  -e POSTGRES_PASSWORD=secure-password \
  postgres:15

# Only expose port on specific interface
docker run -d \
  --name postgres-secure \
  --network postgres-network \
  -p 127.0.0.1:5432:5432 \
  -e POSTGRES_PASSWORD=secure-password \
  postgres:15

Application containers access PostgreSQL through Docker network while external access requires explicit port binding.

SSL/TLS Configuration

Enable encrypted connections:

# Generate SSL certificate
openssl req -new -x509 -days 365 -nodes \
  -text -out server.crt \
  -keyout server.key \
  -subj "/CN=postgres-server"

# Configure PostgreSQL for SSL
cat >> postgresql.conf <<EOF
ssl = on
ssl_cert_file = '/etc/ssl/certs/server.crt'
ssl_key_file = '/etc/ssl/private/server.key'
EOF

# Update pg_hba.conf for SSL required
hostssl all all 0.0.0.0/0 scram-sha-256

SSL encryption protects data in transit between applications and PostgreSQL. Certificate verification prevents man-in-the-middle attacks.

Role-Based Access Control

Implement principle of least privilege:

# Create application role
CREATE ROLE appuser LOGIN PASSWORD 'secure-password';
GRANT CONNECT ON DATABASE appdb TO appuser;
GRANT USAGE ON SCHEMA public TO appuser;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO appuser;

# Create readonly role
CREATE ROLE readonly LOGIN PASSWORD 'readonly-password';
GRANT CONNECT ON DATABASE appdb TO readonly;
GRANT USAGE ON SCHEMA public TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;

# Create monitoring role
CREATE ROLE monitor LOGIN PASSWORD 'monitor-password';
GRANT CONNECT ON DATABASE appdb TO monitor;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO monitor;
GRANT EXECUTE ON FUNCTION pg_stat_activity() TO monitor;

Application roles receive minimum permissions required for operation. Separate read-only roles enable monitoring without write access.

Conclusion

PostgreSQL containers provide flexible database deployment suitable for development through production. Configuration management, backup strategies, and security hardening ensure reliable database operations. High availability through streaming replication protects against single points of failure.

Evaluate managed PostgreSQL services when operational overhead exceeds team capacity. Self-hosted deployments provide maximum control but require ongoing maintenance attention. Consider Patroni for automated failover when high availability is critical.


Related Posts: