Running PostgreSQL in Docker Compose is the standard way to add a database to any containerized application. You get a reproducible database setup that runs identically on every developer’s machine and your production server.
This guide covers the full setup — from a minimal working configuration to production-ready patterns with health checks, persistent storage, backups, and pgAdmin.
Minimal Working Configuration
The shortest docker-compose.yml that runs Postgres:
services:
db:
image: postgres:16
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydb
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Start it:
docker compose up -d
Connect from your host machine:
psql -h localhost -U myuser -d mydb
The named volume postgres_data persists your data across container restarts and docker compose down. It’s only deleted if you explicitly run docker compose down -v.
Environment Variables Reference
The official postgres Docker image recognizes these environment variables:
| Variable | Required | Description |
|---|---|---|
POSTGRES_PASSWORD | Yes | Superuser password |
POSTGRES_USER | No | Superuser name (default: postgres) |
POSTGRES_DB | No | Default database name (default: same as POSTGRES_USER) |
POSTGRES_INITDB_ARGS | No | Extra arguments to initdb |
PGDATA | No | Override data directory location |
The POSTGRES_PASSWORD variable is mandatory. The container will fail to start without it unless you set POSTGRES_HOST_AUTH_METHOD=trust (do not do this in production).
Using an .env File
Never hardcode credentials in docker-compose.yml. Use an .env file instead:
.env (add to .gitignore):
POSTGRES_USER=myuser
POSTGRES_PASSWORD=change_me_in_production
POSTGRES_DB=mydb
docker-compose.yml:
services:
db:
image: postgres:16
env_file:
- .env
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Docker Compose automatically loads .env from the current directory. Any variable defined there is available to containers.
Full Application Stack: App + Postgres
The common pattern — your application service depends on Postgres:
services:
app:
image: your-app:latest
environment:
DATABASE_URL: postgresql://myuser:mypassword@db:5432/mydb
depends_on:
db:
condition: service_healthy
ports:
- "8080:8080"
db:
image: postgres:16
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
postgres_data:
The depends_on with condition: service_healthy ensures your app container doesn’t start until Postgres passes its health check. Without this, your app often starts before Postgres is ready to accept connections.
Health Check Explained
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
pg_isready— the official Postgres readiness check tool, included in the imageinterval— how often to run the checktimeout— how long to wait for a responseretries— how many consecutive failures before marking unhealthystart_period— grace period before counting failures (Postgres takes a few seconds to initialize)
Check health status:
docker compose ps
A healthy container shows healthy in the Status column.
Exposing Postgres (and When Not To)
# Expose to host machine (for local development tools like TablePlus, pgAdmin)
ports:
- "5432:5432"
# Or restrict to a specific host interface
ports:
- "127.0.0.1:5432:5432"
In production, do not expose port 5432 to the public internet. Use one of these approaches instead:
- SSH tunnel:
ssh -L 5432:localhost:5432 user@your-server - Internal Docker network only: remove the
ports:section; your app connects via the service namedb - Jump host or VPN: expose Postgres only on a private network interface
Initializing the Database
Place .sql or .sh files in /docker-entrypoint-initdb.d/ — Postgres runs them on first startup:
services:
db:
image: postgres:16
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init:/docker-entrypoint-initdb.d
volumes:
postgres_data:
./init/01_schema.sql:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id),
title TEXT NOT NULL,
body TEXT,
published_at TIMESTAMPTZ
);
These init scripts only run when the data directory is empty (i.e., first container start). They won’t re-run on subsequent restarts.
Adding pgAdmin for a GUI
pgAdmin is the official Postgres web UI. Add it as another service:
services:
db:
image: postgres:16
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
pgadmin:
image: dpage/pgadmin4:latest
environment:
PGADMIN_DEFAULT_EMAIL: admin@example.com
PGADMIN_DEFAULT_PASSWORD: pgadmin_password
ports:
- "5050:80"
depends_on:
- db
volumes:
postgres_data:
Open http://localhost:5050. Log in with the credentials above, then add a server with:
- Host:
db(the Docker service name) - Port:
5432 - Username:
myuser - Password:
mypassword
Running psql Inside the Container
You don’t need a local Postgres client to run queries:
# Open an interactive psql session
docker compose exec db psql -U myuser -d mydb
# Run a single query
docker compose exec db psql -U myuser -d mydb -c "SELECT COUNT(*) FROM users;"
# Run a SQL file
docker compose exec -T db psql -U myuser -d mydb < query.sql
Database Backups
Manual backup
docker compose exec -T db pg_dump -U myuser mydb > backup_$(date +%Y%m%d_%H%M%S).sql
Restore from backup
cat backup_20260624.sql | docker compose exec -T db psql -U myuser -d mydb
Custom format backup (smaller, faster restore)
# Backup
docker compose exec -T db pg_dump -U myuser -Fc mydb > backup.dump
# Restore
docker compose exec -T db pg_restore -U myuser -d mydb < backup.dump
Automated daily backups with a script
Create /opt/scripts/backup-postgres.sh:
#!/bin/bash
BACKUP_DIR="/opt/backups/postgres"
COMPOSE_DIR="/opt/myapp"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"
cd "$COMPOSE_DIR"
docker compose exec -T db pg_dump -U myuser -Fc mydb > "$BACKUP_DIR/mydb_$TIMESTAMP.dump"
# Keep only last 7 days
find "$BACKUP_DIR" -name "*.dump" -mtime +7 -delete
echo "Backup completed: mydb_$TIMESTAMP.dump"
Add to cron: 0 3 * * * /opt/scripts/backup-postgres.sh >> /var/log/pg-backup.log 2>&1
Performance Tuning
The default Postgres configuration is conservative. For a dedicated database container, tune these settings:
services:
db:
image: postgres:16
command: >
postgres
-c shared_buffers=256MB
-c effective_cache_size=768MB
-c max_connections=100
-c work_mem=4MB
-c maintenance_work_mem=64MB
-c wal_buffers=16MB
-c checkpoint_completion_target=0.9
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
Alternatively, mount a custom postgresql.conf:
volumes:
- ./postgresql.conf:/etc/postgresql/postgresql.conf
command: postgres -c config_file=/etc/postgresql/postgresql.conf
General tuning guidelines for a 4 GB RAM server:
shared_buffers: 25% of RAM (1 GB)effective_cache_size: 75% of RAM (3 GB)work_mem:(RAM / max_connections) / 4
Production Postgres on a VPS
Running Postgres on a $5/month shared VPS is fine for development and small production loads. As your data grows, you’ll need dedicated infrastructure.
Hetzner Cloud offers dedicated vCPU servers starting at €11.21/month with local NVMe storage — much better for database I/O than cloud instances with network-attached storage. New accounts get €20 free credits.
For managed Postgres that handles backups, replication, and failover automatically, DigitalOcean Managed Databases start at $15/month. New accounts get $200 free credits to evaluate the service.
Upgrading Postgres Version
Postgres major version upgrades require a data migration — you can’t just change the image tag from postgres:15 to postgres:16 and restart.
The clean approach:
# 1. Dump all databases
docker compose exec -T db pg_dumpall -U postgres > full_backup.sql
# 2. Update image tag in docker-compose.yml
# 3. Remove the old data volume
docker compose down -v
# 4. Start with the new version (fresh data directory)
docker compose up -d
# 5. Restore
cat full_backup.sql | docker compose exec -T db psql -U postgres
Quick Reference
| Task | Command |
|---|---|
| Start Postgres | docker compose up -d db |
| Open psql shell | docker compose exec db psql -U user -d dbname |
| Run SQL query | docker compose exec db psql -U user -d dbname -c "SQL" |
| Dump database | docker compose exec -T db pg_dump -U user dbname > out.sql |
| Restore database | cat out.sql | docker compose exec -T db psql -U user -d dbname |
| Check health | docker compose ps db |
| View logs | docker compose logs -f db |
| Wipe data | docker compose down -v |
Summary
The key points for a production-ready Postgres Docker Compose setup:
- Always use named volumes — never bind-mount
/var/lib/postgresql/data - Store credentials in
.env, never in the compose file - Add a health check and use
depends_on: condition: service_healthyin dependent services - Do not expose port 5432 to the public internet in production
- Set up automated daily backups before any data matters
- Tune
shared_buffersandeffective_cache_sizefor your server’s RAM
With these patterns, your Postgres container behaves like a reliable production database from day one.