pg_isready, Docker secrets for passwords, and init scripts in /docker-entrypoint-initdb.d/ for schema bootstrapping.docker compose up -d with a docker-compose.yml defining postgres and pgadmin services on a shared network._FILE suffix/docker-entrypoint-initdb.d only run on first startup with an EMPTY data directory/var/lib/postgresql/18/docker -- do NOT hardcode /var/lib/postgresql/data for pg18+| Service | Image | Ports | Volumes | Key Env |
|---|---|---|---|---|
| PostgreSQL | postgres:17 | 5432:5432 | pgdata:/var/lib/postgresql/data | POSTGRES_PASSWORD, POSTGRES_USER, POSTGRES_DB |
| pgAdmin 4 | dpage/pgadmin4:latest | 5050:80 | pgadmin_data:/var/lib/pgadmin | PGADMIN_DEFAULT_EMAIL, PGADMIN_DEFAULT_PASSWORD |
| Variable | Required | Default | Description |
|---|---|---|---|
POSTGRES_PASSWORD | Yes | -- | Superuser password (only required var) |
POSTGRES_USER | No | postgres | Superuser name and default DB name |
POSTGRES_DB | No | Value of POSTGRES_USER | Default database name |
POSTGRES_INITDB_ARGS | No | -- | Extra args to initdb |
POSTGRES_HOST_AUTH_METHOD | No | scram-sha-256 | Auth method for host connections |
PGDATA | No | /var/lib/postgresql/data | Data directory path |
| Variable | Required | Default | Description |
|---|---|---|---|
PGADMIN_DEFAULT_EMAIL | Yes | -- | Admin login email |
PGADMIN_DEFAULT_PASSWORD | Yes | -- | Admin login password |
PGADMIN_LISTEN_PORT | No | 80 | Internal listen port |
PGADMIN_SERVER_JSON_FILE | No | /pgadmin4/servers.json | Auto-register server definitions |
PGADMIN_DISABLE_POSTFIX | No | unset | Set to disable mail server |
PGADMIN_CONFIG_* | No | -- | Override any pgAdmin config.py setting |
START: What environment are you deploying to?
├── Development (local)?
│ ├── YES → Use basic docker-compose.yml with env vars, no secrets needed
│ │ Map ports 5432 + 5050 to localhost
│ └── NO ↓
├── CI/Testing?
│ ├── YES → Use tmpfs for data (no persistence), health checks for readiness
│ │ Skip pgAdmin (use psql CLI instead)
│ └── NO ↓
├── Production / Staging?
│ ├── YES → Use Docker secrets for passwords (_FILE suffix)
│ │ Named volumes with backup strategy
│ │ Add health checks + restart policies
│ │ Restrict pgAdmin to internal network or disable
│ └── NO ↓
└── DEFAULT → Start with development config, then harden
Create a directory with the Compose file and optional init scripts. [src3]
mkdir -p postgres-pgadmin/{init-scripts,pgadmin-servers}
cd postgres-pgadmin
Verify: ls → should show init-scripts/ and pgadmin-servers/ directories.
Create the core Compose file defining both services with health checks, named volumes, and a shared network. [src1] [src2]
Full script: docker-compose.yml (47 lines)
# docker-compose.yml -- PostgreSQL 17 + pgAdmin 4
services:
postgres:
image: postgres:17
container_name: pg-db
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: changeme
POSTGRES_DB: appdb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d appdb"]
interval: 10s
timeout: 5s
retries: 5
Verify: docker compose config → should parse without errors.
Scripts placed in /docker-entrypoint-initdb.d/ run alphabetically on first startup only. Supports .sql, .sql.gz, .sql.xz, and .sh files. [src7]
Full script: 01-init.sql (25 lines)
-- 01-init.sql: Create application schema and default user
CREATE SCHEMA IF NOT EXISTS app;
CREATE USER app_user WITH PASSWORD 'app_password';
GRANT USAGE ON SCHEMA app TO app_user;
GRANT CREATE ON SCHEMA app TO app_user;
Verify: docker compose exec postgres psql -U postgres -d appdb -c '\dt app.*' → should list the tables.
Create a servers.json file so pgAdmin automatically registers the PostgreSQL server on first launch. [src2]
{
"Servers": {
"1": {
"Name": "Local PostgreSQL",
"Group": "Docker",
"Host": "postgres",
"Port": 5432,
"MaintenanceDB": "postgres",
"Username": "postgres",
"PassFile": "/pgadmin4/pgpass",
"SSLMode": "prefer"
}
}
}
Verify: Open http://localhost:5050 → pgAdmin should show "Local PostgreSQL" under the "Docker" server group.
Launch all services and verify they are healthy. [src3]
# Start in detached mode
docker compose up -d
# Wait for health checks to pass
docker compose ps
# Check logs
docker compose logs postgres
docker compose logs pgadmin
Verify: docker compose ps → both services should show healthy status.
Create a backup script that runs pg_dump inside the running container. [src5]
#!/bin/bash
# backup.sh -- Dump PostgreSQL to compressed SQL
CONTAINER="pg-db"
docker exec "$CONTAINER" pg_dump -U postgres -d appdb \
--no-owner --clean --if-exists | gzip -9 \
> "backups/appdb_$(date +%Y%m%d_%H%M%S).sql.gz"
Verify: ls -la backups/ → should show the timestamped .sql.gz file.
Full script: docker-compose-production.yml (53 lines)
# Uses Docker secrets -- no plain text passwords
# Requires: echo "password" > ./secrets/postgres_password
services:
postgres:
image: postgres:17
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
secrets:
- pg_password
ports:
- "127.0.0.1:5432:5432"
secrets:
pg_password:
file: ./secrets/postgres_password
# docker-compose.test.yml -- Ephemeral PostgreSQL for CI
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_PASSWORD: test_password
POSTGRES_DB: test_db
tmpfs:
- /var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 2s
timeout: 5s
retries: 10
ports:
- "5432:5432"
#!/bin/bash
# restore.sh -- Restore PostgreSQL from compressed SQL dump
BACKUP_FILE="$1"
[ -z "$BACKUP_FILE" ] && { echo "Usage: ./restore.sh <file.sql.gz>"; exit 1; }
gunzip -c "$BACKUP_FILE" | docker exec -i pg-db psql -U postgres -d appdb
echo "Restore complete from: $BACKUP_FILE"
import psycopg2 # psycopg2-binary==2.9.x
import time, os
def wait_for_postgres(max_retries=30, delay=2):
dsn = os.environ.get("DATABASE_URL",
"postgresql://postgres:changeme@localhost:5432/appdb")
for attempt in range(max_retries):
try:
conn = psycopg2.connect(dsn)
conn.close()
return True
except psycopg2.OperationalError:
time.sleep(delay)
raise TimeoutError("PostgreSQL not ready")
# BAD -- passwords visible in version control and docker inspect
services:
postgres:
image: postgres:17
environment:
POSTGRES_PASSWORD: my_secret_password # Exposed in git history!
# GOOD -- passwords loaded from external file, not committed
services:
postgres:
image: postgres:17
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
secrets:
- pg_password
secrets:
pg_password:
file: ./secrets/postgres_password # Add to .gitignore
# BAD -- depends_on alone only waits for container to START, not be READY
services:
app:
depends_on:
- postgres # PostgreSQL may not be accepting connections yet!
# GOOD -- waits for PostgreSQL to pass health check
services:
app:
depends_on:
postgres:
condition: service_healthy
postgres:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
# BAD -- bind mounts cause permission issues on macOS/Windows
services:
postgres:
volumes:
- ./pgdata:/var/lib/postgresql/data # Permission errors likely
# GOOD -- named volumes managed by Docker, no permission issues
services:
postgres:
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
driver: local
# BAD -- PostgreSQL accessible from any network interface
ports:
- "5432:5432" # Binds to 0.0.0.0 -- exposed to LAN/internet
# GOOD -- PostgreSQL only accessible from localhost
ports:
- "127.0.0.1:5432:5432"
# OR: Don't expose ports -- let containers connect via Docker network
/docker-entrypoint-initdb.d/ only execute when the data volume is empty. If you change a script, you must remove the volume first: docker compose down -v && docker compose up -d. [src1]sudo chown -R 5050:5050 ./pgadmin-data/ or use a named volume. [src2]depends_on without condition: service_healthy only waits for container start, not readiness. Fix: Add a healthcheck with pg_isready and use condition: service_healthy. [src3]-v flag removes named volumes. Fix: Never use -v unless you intend to destroy all data. [src4]/var/lib/postgresql/18/docker instead of /var/lib/postgresql/data. Fix: Update volume mount path when upgrading. [src1]PGADMIN_REPLACE_SERVERS_ON_STARTUP=True. [src2]postgres (service name) as host, not localhost. [src3]postgres:17 is ~400MB. Fix: Use postgres:17-alpine (~80MB) for development/CI. Note: Alpine uses musl libc. [src6]# Check if PostgreSQL is accepting connections
docker compose exec postgres pg_isready -U postgres
# Connect to PostgreSQL via psql
docker compose exec postgres psql -U postgres -d appdb
# List all databases
docker compose exec postgres psql -U postgres -c '\l'
# List tables in a database
docker compose exec postgres psql -U postgres -d appdb -c '\dt'
# Check PostgreSQL logs
docker compose logs --tail 50 postgres
# Check pgAdmin logs
docker compose logs --tail 50 pgadmin
# Inspect container resource usage
docker stats pg-db pg-admin
# Check volume disk usage
docker system df -v | grep pgdata
# Verify health check status
docker inspect --format='{{json .State.Health}}' pg-db
# Check PostgreSQL config settings
docker compose exec postgres psql -U postgres \
-c 'SHOW ALL' | grep -i "max_connections\|shared_buffers\|work_mem"
| Component | Version | Status | Notes |
|---|---|---|---|
| PostgreSQL 18 | 18.x | Latest | Version-specific PGDATA path |
| PostgreSQL 17 | 17.x | Current stable | Recommended for production |
| PostgreSQL 16 | 16.x | Supported | LTS, widely deployed |
| PostgreSQL 15 | 15.x | Supported | MERGE command introduced |
| PostgreSQL 14 | 14.x | Supported | scram-sha-256 default auth |
| PostgreSQL 13 | 13.x | EOL Nov 2025 | Upgrade recommended |
| pgAdmin 4 v9.x | 9.12 | Current | React 19 UI, python:3-alpine base |
| pgAdmin 4 v8.x | 8.14 | Previous | Legacy UI framework |
| Docker Compose V2 | 2.x | Current | Plugin-based (docker compose) |
| Docker Compose V1 | 1.29 | Deprecated | Standalone (docker-compose) |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Local development requiring PostgreSQL + GUI admin | Production database needing managed service | AWS RDS, Cloud SQL, Supabase |
| CI/CD pipeline needs ephemeral PostgreSQL | Need multi-region database replication | PostgreSQL on Kubernetes with Patroni |
| Quick prototyping or demo environments | Team prefers CLI-only database management | Just postgres service + psql |
| Teaching/learning PostgreSQL administration | Need advanced monitoring dashboards | Prometheus + Grafana stack |
| Self-hosted staging environments | Need automated failover and HA | pgBouncer + Patroni + etcd |
postgres:latest tag may jump major versions unexpectedly -- always pin a specific major version (e.g., postgres:17) in production:delegated mount options for development/var/lib/pgadmin -- losing this volume means re-entering all saved queries and preferencesPOSTGRES_HOST_AUTH_METHOD=trust setting disables ALL password authentication and should NEVER be used outside of throwaway test containerspg_dump and restore -- in-place upgrades of the data directory between major versions are not supported via Docker