Skip to content

PostgreSQL Containerization Best Practices

Objective: Master senior-level PostgreSQL containerization patterns for production systems. When you need to containerize PostgreSQL, when you want to implement Docker best practices, when you need enterprise-grade container strategies—these best practices become your weapon of choice.

Core Principles

  • Immutable Infrastructure: Use containers for consistent deployments
  • Security: Implement container security best practices
  • Performance: Optimize container resource usage
  • Scalability: Design for container orchestration
  • Monitoring: Track container health and performance

Docker Configuration

Multi-Stage Dockerfile

# Dockerfile for PostgreSQL with extensions
FROM postgres:15-alpine AS base

# Install system dependencies
RUN apk add --no-cache \
    build-base \
    postgresql-dev \
    postgresql-contrib \
    postgis \
    && rm -rf /var/cache/apk/*

# Install PostgreSQL extensions
RUN echo "CREATE EXTENSION IF NOT EXISTS postgis;" > /docker-entrypoint-initdb.d/01-postgis.sql
RUN echo "CREATE EXTENSION IF NOT EXISTS pg_stat_statements;" > /docker-entrypoint-initdb.d/02-pg_stat_statements.sql
RUN echo "CREATE EXTENSION IF NOT EXISTS pgcrypto;" > /docker-entrypoint-initdb.d/03-pgcrypto.sql

# Production stage
FROM postgres:15-alpine AS production

# Copy extensions from base stage
COPY --from=base /usr/local/lib/postgresql/* /usr/local/lib/postgresql/
COPY --from=base /usr/local/share/postgresql/extension/* /usr/local/share/postgresql/extension/

# Set environment variables
ENV POSTGRES_DB=production
ENV POSTGRES_USER=postgres
ENV POSTGRES_PASSWORD=secure_password
ENV POSTGRES_INITDB_ARGS="--auth-host=scram-sha-256"

# Create custom configuration
COPY postgresql.conf /etc/postgresql/postgresql.conf
COPY pg_hba.conf /etc/postgresql/pg_hba.conf

# Set proper permissions
RUN chown -R postgres:postgres /etc/postgresql
RUN chmod 600 /etc/postgresql/postgresql.conf
RUN chmod 600 /etc/postgresql/pg_hba.conf

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD pg_isready -U postgres -d production || exit 1

# Expose port
EXPOSE 5432

# Use custom configuration
CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"]

Docker Compose Configuration

# docker-compose.yml
version: '3.8'

services:
  postgres:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    container_name: postgres-production
    restart: unless-stopped
    environment:
      POSTGRES_DB: production
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./config/postgresql.conf:/etc/postgresql/postgresql.conf:ro
      - ./config/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
      - ./scripts:/docker-entrypoint-initdb.d:ro
      - ./backups:/backups
    ports:
      - "5432:5432"
    networks:
      - postgres_network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d production"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    deploy:
      resources:
        limits:
          memory: 2G
          cpus: '1.0'
        reservations:
          memory: 1G
          cpus: '0.5'
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp
      - /var/run/postgresql

  postgres-replica:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    container_name: postgres-replica
    restart: unless-stopped
    environment:
      POSTGRES_DB: production
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      PGUSER: postgres
      PGPASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_replica_data:/var/lib/postgresql/data
      - ./config/postgresql-replica.conf:/etc/postgresql/postgresql.conf:ro
      - ./config/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
    ports:
      - "5433:5432"
    networks:
      - postgres_network
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d production"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: pgadmin
    restart: unless-stopped
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@example.com
      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD}
      PGADMIN_CONFIG_SERVER_MODE: 'False'
    volumes:
      - pgadmin_data:/var/lib/pgadmin
    ports:
      - "8080:80"
    networks:
      - postgres_network
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  postgres_data:
    driver: local
  postgres_replica_data:
    driver: local
  pgadmin_data:
    driver: local

networks:
  postgres_network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16

Container Security

Security Configuration

-- Create container security configuration table
CREATE TABLE container_security_config (
    id SERIAL PRIMARY KEY,
    config_name VARCHAR(100) UNIQUE NOT NULL,
    security_level VARCHAR(20) NOT NULL,
    config_data JSONB NOT NULL,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

-- Create function to configure container security
CREATE OR REPLACE FUNCTION configure_container_security(
    p_config_name VARCHAR(100),
    p_security_level VARCHAR(20),
    p_config_data JSONB
)
RETURNS VOID AS $$
BEGIN
    INSERT INTO container_security_config (
        config_name, security_level, config_data
    ) VALUES (
        p_config_name, p_security_level, p_config_data
    ) ON CONFLICT (config_name) 
    DO UPDATE SET 
        security_level = EXCLUDED.security_level,
        config_data = EXCLUDED.config_data;
END;
$$ LANGUAGE plpgsql;

-- Create function to get security configuration
CREATE OR REPLACE FUNCTION get_security_config(p_config_name VARCHAR(100))
RETURNS TABLE (
    config_name VARCHAR(100),
    security_level VARCHAR(20),
    config_data JSONB
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        csc.config_name, csc.security_level, csc.config_data
    FROM container_security_config csc
    WHERE csc.config_name = p_config_name AND csc.is_active = TRUE;
END;
$$ LANGUAGE plpgsql;

Network Security

-- Create network security configuration table
CREATE TABLE network_security_config (
    id SERIAL PRIMARY KEY,
    network_name VARCHAR(100) NOT NULL,
    security_policy JSONB NOT NULL,
    allowed_ports INTEGER[],
    denied_ports INTEGER[],
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

-- Create function to configure network security
CREATE OR REPLACE FUNCTION configure_network_security(
    p_network_name VARCHAR(100),
    p_security_policy JSONB,
    p_allowed_ports INTEGER[] DEFAULT NULL,
    p_denied_ports INTEGER[] DEFAULT NULL
)
RETURNS VOID AS $$
BEGIN
    INSERT INTO network_security_config (
        network_name, security_policy, allowed_ports, denied_ports
    ) VALUES (
        p_network_name, p_security_policy, p_allowed_ports, p_denied_ports
    ) ON CONFLICT (network_name) 
    DO UPDATE SET 
        security_policy = EXCLUDED.security_policy,
        allowed_ports = EXCLUDED.allowed_ports,
        denied_ports = EXCLUDED.denied_ports;
END;
$$ LANGUAGE plpgsql;

Container Orchestration

Kubernetes Configuration

# postgres-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  labels:
    app: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 999
        fsGroup: 999
      containers:
      - name: postgres
        image: postgres:15-alpine
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_DB
          value: production
        - name: POSTGRES_USER
          value: postgres
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgres-secret
              key: password
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
        - name: postgres-config
          mountPath: /etc/postgresql
        resources:
          requests:
            memory: "1Gi"
            cpu: "500m"
          limits:
            memory: "2Gi"
            cpu: "1000m"
        livenessProbe:
          exec:
            command:
            - pg_isready
            - -U
            - postgres
            - -d
            - production
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          exec:
            command:
            - pg_isready
            - -U
            - postgres
            - -d
            - production
          initialDelaySeconds: 5
          periodSeconds: 5
      volumes:
      - name: postgres-storage
        persistentVolumeClaim:
          claimName: postgres-pvc
      - name: postgres-config
        configMap:
          name: postgres-config
---
apiVersion: v1
kind: Service
metadata:
  name: postgres-service
spec:
  selector:
    app: postgres
  ports:
  - port: 5432
    targetPort: 5432
  type: ClusterIP
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
---
apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
type: Opaque
data:
  password: c2VjdXJlX3Bhc3N3b3Jk  # base64 encoded
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-config
data:
  postgresql.conf: |
    # Connection settings
    listen_addresses = '*'
    port = 5432
    max_connections = 100

    # Memory settings
    shared_buffers = 256MB
    effective_cache_size = 1GB
    work_mem = 4MB

    # Logging settings
    log_destination = 'stderr'
    logging_collector = on
    log_directory = '/var/log/postgresql'
    log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
    log_rotation_age = 1d
    log_rotation_size = 100MB

    # Performance settings
    checkpoint_completion_target = 0.9
    wal_buffers = 16MB
    default_statistics_target = 100

Docker Swarm Configuration

# docker-stack.yml
version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.role == manager
      resources:
        limits:
          memory: 2G
          cpus: '1.0'
        reservations:
          memory: 1G
          cpus: '0.5'
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
        window: 120s
    environment:
      POSTGRES_DB: production
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./config/postgresql.conf:/etc/postgresql/postgresql.conf:ro
    networks:
      - postgres_network
    secrets:
      - postgres_password
    configs:
      - postgres_config

  postgres-replica:
    image: postgres:15-alpine
    deploy:
      replicas: 2
      placement:
        constraints:
          - node.role == worker
      resources:
        limits:
          memory: 1G
          cpus: '0.5'
        reservations:
          memory: 512M
          cpus: '0.25'
    environment:
      POSTGRES_DB: production
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_replica_data:/var/lib/postgresql/data
    networks:
      - postgres_network
    depends_on:
      - postgres
    secrets:
      - postgres_password

volumes:
  postgres_data:
    driver: local
  postgres_replica_data:
    driver: local

networks:
  postgres_network:
    driver: overlay
    attachable: true

secrets:
  postgres_password:
    external: true

configs:
  postgres_config:
    external: true

Container Monitoring

Health Checks

-- Create container health monitoring table
CREATE TABLE container_health_monitoring (
    id SERIAL PRIMARY KEY,
    container_id VARCHAR(100) NOT NULL,
    container_name VARCHAR(100) NOT NULL,
    health_status VARCHAR(20) NOT NULL,
    cpu_usage NUMERIC,
    memory_usage NUMERIC,
    disk_usage NUMERIC,
    network_io NUMERIC,
    recorded_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

-- Create function to record container health
CREATE OR REPLACE FUNCTION record_container_health(
    p_container_id VARCHAR(100),
    p_container_name VARCHAR(100),
    p_health_status VARCHAR(20),
    p_cpu_usage NUMERIC DEFAULT NULL,
    p_memory_usage NUMERIC DEFAULT NULL,
    p_disk_usage NUMERIC DEFAULT NULL,
    p_network_io NUMERIC DEFAULT NULL
)
RETURNS VOID AS $$
BEGIN
    INSERT INTO container_health_monitoring (
        container_id, container_name, health_status,
        cpu_usage, memory_usage, disk_usage, network_io
    ) VALUES (
        p_container_id, p_container_name, p_health_status,
        p_cpu_usage, p_memory_usage, p_disk_usage, p_network_io
    );
END;
$$ LANGUAGE plpgsql;

-- Create function to get container health report
CREATE OR REPLACE FUNCTION get_container_health_report(
    p_container_name VARCHAR(100),
    p_start_date TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP - INTERVAL '1 hour',
    p_end_date TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
)
RETURNS TABLE (
    container_name VARCHAR(100),
    avg_cpu_usage NUMERIC,
    avg_memory_usage NUMERIC,
    avg_disk_usage NUMERIC,
    avg_network_io NUMERIC,
    health_status_count BIGINT
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        chm.container_name,
        AVG(chm.cpu_usage) as avg_cpu_usage,
        AVG(chm.memory_usage) as avg_memory_usage,
        AVG(chm.disk_usage) as avg_disk_usage,
        AVG(chm.network_io) as avg_network_io,
        COUNT(*) as health_status_count
    FROM container_health_monitoring chm
    WHERE chm.container_name = p_container_name
    AND chm.recorded_at BETWEEN p_start_date AND p_end_date
    GROUP BY chm.container_name;
END;
$$ LANGUAGE plpgsql;

Container Implementation

Python Container Manager

# containerization/postgres_container_manager.py
import psycopg2
import json
import docker
import subprocess
from datetime import datetime
from typing import Dict, List, Optional
import logging

class PostgreSQLContainerManager:
    def __init__(self, connection_params):
        self.conn_params = connection_params
        self.docker_client = docker.from_env()
        self.setup_logging()

    def setup_logging(self):
        """Setup logging."""
        logging.basicConfig(level=logging.INFO)
        self.logger = logging.getLogger(__name__)

    def get_connection(self):
        """Get database connection."""
        return psycopg2.connect(**self.conn_params)

    def create_postgres_container(self, container_name: str, 
                                 postgres_password: str, 
                                 postgres_db: str = 'production',
                                 postgres_user: str = 'postgres',
                                 port: int = 5432,
                                 volumes: Dict[str, str] = None,
                                 environment: Dict[str, str] = None):
        """Create PostgreSQL container."""
        try:
            # Default volumes
            if volumes is None:
                volumes = {
                    'postgres_data': '/var/lib/postgresql/data',
                    './config/postgresql.conf': '/etc/postgresql/postgresql.conf:ro'
                }

            # Default environment
            if environment is None:
                environment = {
                    'POSTGRES_DB': postgres_db,
                    'POSTGRES_USER': postgres_user,
                    'POSTGRES_PASSWORD': postgres_password
                }

            # Create container
            container = self.docker_client.containers.run(
                'postgres:15-alpine',
                name=container_name,
                environment=environment,
                ports={5432: port},
                volumes=volumes,
                detach=True,
                restart_policy={'Name': 'unless-stopped'},
                healthcheck={
                    'test': ['CMD-SHELL', 'pg_isready -U postgres -d production'],
                    'interval': 30000000000,  # 30 seconds in nanoseconds
                    'timeout': 10000000000,   # 10 seconds in nanoseconds
                    'retries': 3,
                    'start_period': 40000000000  # 40 seconds in nanoseconds
                }
            )

            self.logger.info(f"PostgreSQL container {container_name} created successfully")
            return container

        except Exception as e:
            self.logger.error(f"Error creating PostgreSQL container: {e}")
            raise

    def start_container(self, container_name: str):
        """Start container."""
        try:
            container = self.docker_client.containers.get(container_name)
            container.start()
            self.logger.info(f"Container {container_name} started")

        except Exception as e:
            self.logger.error(f"Error starting container {container_name}: {e}")
            raise

    def stop_container(self, container_name: str):
        """Stop container."""
        try:
            container = self.docker_client.containers.get(container_name)
            container.stop()
            self.logger.info(f"Container {container_name} stopped")

        except Exception as e:
            self.logger.error(f"Error stopping container {container_name}: {e}")
            raise

    def restart_container(self, container_name: str):
        """Restart container."""
        try:
            container = self.docker_client.containers.get(container_name)
            container.restart()
            self.logger.info(f"Container {container_name} restarted")

        except Exception as e:
            self.logger.error(f"Error restarting container {container_name}: {e}")
            raise

    def get_container_status(self, container_name: str):
        """Get container status."""
        try:
            container = self.docker_client.containers.get(container_name)
            status = container.status
            health = container.attrs.get('State', {}).get('Health', {}).get('Status', 'unknown')

            return {
                'status': status,
                'health': health,
                'created': container.attrs['Created'],
                'started_at': container.attrs['State'].get('StartedAt', None)
            }

        except Exception as e:
            self.logger.error(f"Error getting container status: {e}")
            return None

    def get_container_metrics(self, container_name: str):
        """Get container metrics."""
        try:
            container = self.docker_client.containers.get(container_name)
            stats = container.stats(stream=False)

            # Calculate CPU usage
            cpu_delta = stats['cpu_stats']['cpu_usage']['total_usage'] - stats['precpu_stats']['cpu_usage']['total_usage']
            system_delta = stats['cpu_stats']['system_cpu_usage'] - stats['precpu_stats']['system_cpu_usage']
            cpu_usage = (cpu_delta / system_delta) * 100.0 if system_delta > 0 else 0

            # Calculate memory usage
            memory_usage = stats['memory_stats']['usage']
            memory_limit = stats['memory_stats']['limit']
            memory_percentage = (memory_usage / memory_limit) * 100.0 if memory_limit > 0 else 0

            # Calculate disk usage
            disk_usage = 0
            for device in stats['blkio_stats']['io_service_bytes_recursive']:
                if device['op'] == 'Read':
                    disk_usage += device['value']
                elif device['op'] == 'Write':
                    disk_usage += device['value']

            # Calculate network I/O
            network_io = 0
            for network in stats['networks'].values():
                network_io += network['rx_bytes'] + network['tx_bytes']

            return {
                'cpu_usage': cpu_usage,
                'memory_usage': memory_percentage,
                'disk_usage': disk_usage,
                'network_io': network_io
            }

        except Exception as e:
            self.logger.error(f"Error getting container metrics: {e}")
            return None

    def record_container_health(self, container_name: str, health_status: str, 
                               metrics: Dict[str, float]):
        """Record container health in database."""
        conn = self.get_connection()

        try:
            with conn.cursor() as cur:
                cur.execute("""
                    SELECT record_container_health(%s, %s, %s, %s, %s, %s, %s)
                """, (
                    container_name, container_name, health_status,
                    metrics.get('cpu_usage'), metrics.get('memory_usage'),
                    metrics.get('disk_usage'), metrics.get('network_io')
                ))

                conn.commit()
                self.logger.info(f"Container health recorded for {container_name}")

        except Exception as e:
            conn.rollback()
            self.logger.error(f"Error recording container health: {e}")
            raise
        finally:
            conn.close()

    def monitor_containers(self, container_names: List[str]):
        """Monitor container health and metrics."""
        for container_name in container_names:
            try:
                # Get container status
                status = self.get_container_status(container_name)
                if not status:
                    continue

                # Get container metrics
                metrics = self.get_container_metrics(container_name)
                if not metrics:
                    continue

                # Record health
                self.record_container_health(container_name, status['health'], metrics)

            except Exception as e:
                self.logger.error(f"Error monitoring container {container_name}: {e}")

    def get_container_health_report(self, container_name: str, 
                                   start_date: datetime = None, 
                                   end_date: datetime = None):
        """Get container health report."""
        conn = self.get_connection()

        try:
            with conn.cursor() as cur:
                cur.execute("""
                    SELECT * FROM get_container_health_report(%s, %s, %s)
                """, (container_name, start_date, end_date))

                report = cur.fetchone()
                if report:
                    return {
                        'container_name': report[0],
                        'avg_cpu_usage': report[1],
                        'avg_memory_usage': report[2],
                        'avg_disk_usage': report[3],
                        'avg_network_io': report[4],
                        'health_status_count': report[5]
                    }
                return None

        except Exception as e:
            self.logger.error(f"Error getting container health report: {e}")
            return None
        finally:
            conn.close()

# Usage
if __name__ == "__main__":
    manager = PostgreSQLContainerManager({
        'host': 'localhost',
        'database': 'production',
        'user': 'container_manager_user',
        'password': 'container_manager_password'
    })

    # Create PostgreSQL container
    container = manager.create_postgres_container(
        'postgres-production',
        'secure_password',
        'production',
        'postgres',
        5432
    )

    # Monitor containers
    manager.monitor_containers(['postgres-production'])

    print("PostgreSQL containerization setup complete")

TL;DR Runbook

Quick Start

# 1. Build PostgreSQL container
docker build -t postgres-custom:latest .

# 2. Run PostgreSQL container
docker run -d --name postgres-production \
  -e POSTGRES_DB=production \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_PASSWORD=secure_password \
  -p 5432:5432 \
  -v postgres_data:/var/lib/postgresql/data \
  postgres-custom:latest

# 3. Check container health
docker ps
docker logs postgres-production

# 4. Monitor container metrics
docker stats postgres-production

Essential Patterns

# Complete PostgreSQL containerization setup
def setup_postgresql_containerization():
    # 1. Docker configuration
    # 2. Container security
    # 3. Container orchestration
    # 4. Container monitoring
    # 5. Health checks
    # 6. Resource management
    # 7. Network security
    # 8. Performance optimization

    print("PostgreSQL containerization setup complete!")

This guide provides the complete machinery for PostgreSQL containerization excellence. Each pattern includes implementation examples, container strategies, and real-world usage patterns for enterprise PostgreSQL containerized systems.