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:

VariableRequiredDescription
POSTGRES_PASSWORDYesSuperuser password
POSTGRES_USERNoSuperuser name (default: postgres)
POSTGRES_DBNoDefault database name (default: same as POSTGRES_USER)
POSTGRES_INITDB_ARGSNoExtra arguments to initdb
PGDATANoOverride 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 image
  • interval — how often to run the check
  • timeout — how long to wait for a response
  • retries — how many consecutive failures before marking unhealthy
  • start_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:

  1. SSH tunnel: ssh -L 5432:localhost:5432 user@your-server
  2. Internal Docker network only: remove the ports: section; your app connects via the service name db
  3. 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

TaskCommand
Start Postgresdocker compose up -d db
Open psql shelldocker compose exec db psql -U user -d dbname
Run SQL querydocker compose exec db psql -U user -d dbname -c "SQL"
Dump databasedocker compose exec -T db pg_dump -U user dbname > out.sql
Restore databasecat out.sql | docker compose exec -T db psql -U user -d dbname
Check healthdocker compose ps db
View logsdocker compose logs -f db
Wipe datadocker compose down -v

Summary

The key points for a production-ready Postgres Docker Compose setup:

  1. Always use named volumes — never bind-mount /var/lib/postgresql/data
  2. Store credentials in .env, never in the compose file
  3. Add a health check and use depends_on: condition: service_healthy in dependent services
  4. Do not expose port 5432 to the public internet in production
  5. Set up automated daily backups before any data matters
  6. Tune shared_buffers and effective_cache_size for your server’s RAM

With these patterns, your Postgres container behaves like a reliable production database from day one.