Running PostgreSQL with CleanStart
CleanStart provides secure PostgreSQL base images (cleanstart/postgresql) with PostgreSQL 14, 15, and 16 versions pre-configured for container environments. These images include FIPS 140-3 mode support for regulated industries, support for replication and backup tooling, and Kubernetes-ready health checks for production deployments.
Quick Start: A Simple PostgreSQL Setup
Step 1: Run PostgreSQL Container
docker run -d \ --name postgres \ -e POSTGRES_USER=appuser \ -e POSTGRES_PASSWORD=mysecurepassword \ -e POSTGRES_DB=myapp \ -v postgres-data:/var/lib/postgresql/data \ -p 5432:5432 \ cleanstart/postgresql:16Step 2: Connect and Initialize Schema
docker exec -it postgres psql -U appuser -d myapp << EOFCREATE TABLE users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); CREATE INDEX idx_users_email ON users(email); INSERT INTO users (email, name) VALUES ('user@example.com', 'John Doe'), ('admin@example.com', 'Admin User');EOFStep 3: Verify Connection
docker exec -it postgres psql -U appuser -d myapp -c "SELECT * FROM users;"Production Dockerfile Setup
Create a custom PostgreSQL image with your schema:
FROM cleanstart/postgresql:16 # Create initialization scriptCOPY init-db.sql /docker-entrypoint-initdb.d/ # Set security parametersENV POSTGRES_INITDB_ARGS="-c max_connections=200 \ -c shared_buffers=256MB \ -c effective_cache_size=1GB \ -c maintenance_work_mem=64MB \ -c random_page_cost=1.1" # Health checkHEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=3 \ CMD pg_isready -U postgres || exit 1Create init-db.sql:
CREATE TABLE users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP); CREATE TABLE sessions ( id UUID PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), token VARCHAR(500) NOT NULL, expires_at TIMESTAMP NOT NULL); CREATE INDEX idx_users_email ON users(email);CREATE INDEX idx_sessions_user_id ON sessions(user_id);CREATE INDEX idx_sessions_expires_at ON sessions(expires_at); -- Enable extensionsCREATE EXTENSION IF NOT EXISTS "uuid-ossp";CREATE EXTENSION IF NOT EXISTS pg_stat_statements;Build and run:
docker build -t my-postgres:latest .docker run -d \ --name my-postgres \ -e POSTGRES_USER=appuser \ -e POSTGRES_PASSWORD='complex-password-here' \ -e POSTGRES_DB=myapp \ -v postgres-data:/var/lib/postgresql/data \ -p 5432:5432 \ my-postgres:latestConfiguration Management
Environment Variables
CleanStart PostgreSQL supports standard variables:
docker run -d \ -e POSTGRES_USER=dbuser \ -e POSTGRES_PASSWORD=secretpassword \ -e POSTGRES_DB=production_db \ -e POSTGRES_INITDB_ARGS="-c max_connections=200" \ cleanstart/postgresql:16Memory and Performance Tuning
FROM cleanstart/postgresql:16 ENV POSTGRES_INITDB_ARGS="\ -c shared_buffers=512MB \ -c effective_cache_size=2GB \ -c maintenance_work_mem=256MB \ -c work_mem=50MB \ -c max_connections=300 \ -c max_parallel_workers=4" # Enable useful extensionsCOPY enable-extensions.sql /docker-entrypoint-initdb.d/ HEALTHCHECK --interval=10s --timeout=5s --retries=3 \ CMD pg_isready -U postgres || exit 1Enable extensions in enable-extensions.sql:
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";CREATE EXTENSION IF NOT EXISTS pg_stat_statements;CREATE EXTENSION IF NOT EXISTS pgcrypto;CREATE EXTENSION IF NOT EXISTS btree_gin;CREATE EXTENSION IF NOT EXISTS btree_gist;Backup Configuration
Create a backup script backup.sh:
#!/bin/bashset -e BACKUP_DIR="/backups"TIMESTAMP=$(date +%Y%m%d_%H%M%S)BACKUP_FILE="$BACKUP_DIR/backup_$TIMESTAMP.sql.gz" mkdir -p "$BACKUP_DIR" # Create backuppg_dump -U $POSTGRES_USER -d $POSTGRES_DB | \ gzip > "$BACKUP_FILE" # Keep only last 7 daysfind "$BACKUP_DIR" -name "backup_*.sql.gz" -mtime +7 -delete echo "Backup completed: $BACKUP_FILE"Add to Dockerfile:
FROM cleanstart/postgresql:16 COPY backup.sh /usr/local/bin/RUN chmod +x /usr/local/bin/backup.sh # Schedule backup with cronRUN apt-get update && apt-get install -y cron && rm -rf /var/lib/apt/lists/*Replication Setup (Primary-Replica)
Primary Server
FROM cleanstart/postgresql:16 COPY primary-config.conf /etc/postgresql/postgresql.conf.d/COPY pg_hba.conf /etc/postgresql/ ENV POSTGRES_INITDB_ARGS="-c config_file=/etc/postgresql/postgresql.conf"primary-config.conf:
wal_level = replicamax_wal_senders = 10max_replication_slots = 10hot_standby = onpg_hba.conf:
# Allow replication from replicahost replication replicator 10.0.0.0/8 scram-sha-256Replica Server
# Create replicadocker run -d \ --name postgres-replica \ -e PGUSER=postgres \ -e PGPASSWORD=replica_password \ cleanstart/postgresql:16 \ pg_basebackup -h postgres-primary -D /var/lib/postgresql/data -U replicator -v -P -WDocker Compose Example
version: '3.9' services: postgres: image: cleanstart/postgresql:16 container_name: my-postgres environment: POSTGRES_USER: appuser POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: myapp POSTGRES_INITDB_ARGS: | -c max_connections=200 -c shared_buffers=256MB ports: - "5432:5432" volumes: - postgres-data:/var/lib/postgresql/data - ./init-db.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD-SHELL", "pg_isready -U appuser"] interval: 10s timeout: 5s retries: 5 networks: - app-network pgadmin: image: dpage/pgadmin4:latest container_name: pgadmin environment: PGADMIN_DEFAULT_EMAIL: admin@example.com PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD} ports: - "5050:80" depends_on: - postgres networks: - app-network volumes: postgres-data: networks: app-network:Run:
export DB_PASSWORD='secure-password'export PGADMIN_PASSWORD='admin-password'docker-compose up -dFIPS 140-3 Compliance
For regulated environments:
docker run -d \ --name postgres-fips \ cleanstart/postgresql:16-fips \ -e POSTGRES_USER=appuser \ -e POSTGRES_PASSWORD=passwordKubernetes Deployment
Create postgres-deployment.yaml:
apiVersion: apps/v1kind: Deploymentmetadata: name: postgresspec: replicas: 1 selector: matchLabels: app: postgres template: metadata: labels: app: postgres spec: containers: - name: postgres image: cleanstart/postgresql:16 env: - name: POSTGRES_USER value: appuser - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: db-credentials key: password - name: POSTGRES_DB value: myapp ports: - containerPort: 5432 livenessProbe: exec: command: - /bin/sh - -c - pg_isready -U appuser initialDelaySeconds: 30 periodSeconds: 10 volumeMounts: - name: data mountPath: /var/lib/postgresql/data - name: init-scripts mountPath: /docker-entrypoint-initdb.d volumes: - name: data persistentVolumeClaim: claimName: postgres-pvc - name: init-scripts configMap: name: postgres-init ---apiVersion: v1kind: Servicemetadata: name: postgresspec: ports: - port: 5432 targetPort: 5432 selector: app: postgres clusterIP: None ---apiVersion: v1kind: PersistentVolumeClaimmetadata: name: postgres-pvcspec: accessModes: - ReadWriteOnce resources: requests: storage: 10GiDeploy:
kubectl create secret generic db-credentials --from-literal=password='secure-password'kubectl apply -f postgres-deployment.yamlSecurity Best Practices
Strong Passwords
Use randomly generated passwords:
openssl rand -base64 32Connection Limits
Connection limits control how your PostgreSQL instance handles concurrent client connections and query workloads. The max_connections parameter sets the total number of simultaneous connections the database will accept (typically 200 for moderate workloads). The max_parallel_workers parameter controls how many workers PostgreSQL can use for parallel query execution (usually 4 on modern hardware). The work_mem parameter determines how much memory each query operation can use before spilling to disk (typically 50MB per operation).
max_connections = 200 # Total connectionsmax_parallel_workers = 4 # Parallel querieswork_mem = 50MB # Per query memoryAuthentication
Change the default password immediately:
docker exec -it postgres psql -U postgres -c "ALTER USER postgres WITH PASSWORD 'newpassword';"Encryption in Transit
Use SSL/TLS for remote connections:
FROM cleanstart/postgresql:16 # Copy SSL certificatesCOPY server.crt /etc/postgresql/COPY server.key /etc/postgresql/ ENV POSTGRES_INITDB_ARGS="-c ssl=on -c ssl_cert_file=/etc/postgresql/server.crt -c ssl_key_file=/etc/postgresql/server.key"Monitoring
Basic Health Check
docker exec postgres pg_isready -U appuserDetailed Status
docker exec -it postgres psql -U appuser -c "SELECT version();"docker exec -it postgres psql -U appuser -c "SELECT datname, numbackends FROM pg_stat_database;"Performance Monitoring
SELECT query, calls, total_time, mean_timeFROM pg_stat_statementsORDER BY total_time DESCLIMIT 10;Troubleshooting
Connection refused errors typically indicate issues with port configuration or network connectivity between services. Verify that port 5432 is properly exposed and accessible from the client. Authentication failures usually stem from mismatched POSTGRES_USER and POSTGRES_PASSWORD values—double-check that the credentials provided at runtime match those configured in your environment variables. Slow query performance can be addressed by checking that appropriate database indexes exist, enabling the pg_stat_statements extension for query analysis, and reviewing execution plans. Out of disk errors require monitoring your data volume size, implementing a retention policy for backups, and cleaning old backup files to reclaim storage space.
Image Options
Image | Use Case |
|---|---|
| Latest stable (recommended) |
| Legacy applications |
| FIPS 140-3 compliance |
CleanStart Production Hardening
Read-Only Root Filesystem
Enforce immutable infrastructure with read-only root, with specific writable paths for PostgreSQL data:
apiVersion: v1kind: Podmetadata: name: postgresqlspec: containers: - name: postgres image: cleanstart/postgresql:16 securityContext: readOnlyRootFilesystem: true volumeMounts: - name: data mountPath: /var/lib/postgresql/data - name: run mountPath: /var/run/postgresql - name: tmp mountPath: /tmp volumes: - name: data persistentVolumeClaim: claimName: postgres-data - name: run emptyDir: {} - name: tmp emptyDir: {}Shell-Less ENTRYPOINT
Remove shell for attack surface reduction. Update your Dockerfile:
FROM cleanstart/postgresql:16 # Copy custom PostgreSQL configurationCOPY postgresql.conf /etc/postgresql/postgresql.conf.d/ # Declarative Image Builder: Use cleanimg-init as PID 1ENTRYPOINT ["/cleanimg-init", "--"]CMD ["postgres"]cleanimg-customize: PostgreSQL Configuration
Inject production-hardened PostgreSQL settings:
FROM cleanstart/postgresql:16 # Create custom configuration directoryRUN mkdir -p /etc/postgresql/conf.d # Inject hardened postgresql.conf settingsRUN echo 'ssl = on' >> /etc/postgresql/postgresql.conf && \ echo 'ssl_protocols = "TLSv1.3"' >> /etc/postgresql/postgresql.conf && \ echo 'max_connections = 100' >> /etc/postgresql/postgresql.conf && \ echo 'shared_buffers = 256MB' >> /etc/postgresql/postgresql.conf ENTRYPOINT ["/cleanimg-init", "--"]CMD ["postgres"]Security Context
Complete Kubernetes securityContext for hardened PostgreSQL containers:
apiVersion: apps/v1kind: StatefulSetmetadata: name: postgresqlspec: template: spec: securityContext: fsGroup: 999 seccompProfile: type: RuntimeDefault containers: - name: postgres image: cleanstart/postgresql:16 securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 999 runAsGroup: 999 capabilities: drop: - ALL add: - NET_BIND_SERVICE resources: requests: cpu: 500m memory: 512Mi limits: cpu: 2000m memory: 2GiNext Steps
Advanced PostgreSQL Configuration, PostgreSQL Replication Guide, and CleanStart Image Registry.
