Postgres Docker Compose: The Complete Setup Guide
Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.
Running PostgreSQL locally used to mean installing packages, fighting with version conflicts, and cleaning up leftover config files. With postgres docker compose, you get a reproducible, disposable database environment that starts with one command and leaves no trace on your host system when you’re done.
This guide covers everything from a minimal single-container setup to a production-ready configuration with persistent volumes, health checks, environment-based secrets, and automated backups. If you’re deploying containerized apps regularly, pair this with our Docker networking guide for a deeper understanding of how services talk to each other inside Compose.
Why Use Docker Compose for Postgres
Docker Compose turns a multi-step manual setup into a declarative YAML file. Instead of remembering flags for docker run, you define the database service, its volumes, networks, and environment variables once, then bring the whole stack up or down with a single command.
This matters for a few practical reasons:
docker-compose.yml produces an identical environment on your laptop, your CI runner, and your staging server.docker compose down -v wipes the database cleanly, which is ideal for testing migrations or resetting a dev environment.If you’re new to containers generally, our Docker for beginners walkthrough covers the fundamentals before you dive into Compose specifics.
Prerequisites
Before starting, make sure you have:
docker compose, not the old standalone docker-compose)Check your versions with:
docker --version
docker compose version
If docker compose version fails, consult the official Docker Compose installation docs for your platform.
Basic Postgres Docker Compose Setup
Start with a minimal working example. Create a project directory and a docker-compose.yml file:
mkdir postgres-compose-demo && cd postgres-compose-demo
touch docker-compose.yml
Add this configuration:
services:
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: changeme
POSTGRES_DB: appdb
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Bring it up:
docker compose up -d
Verify the container is running and healthy:
docker compose ps
docker compose logs db
Connect using psql from your host (if installed) or directly inside the container:
docker compose exec db psql -U appuser -d appdb
That’s a fully working Postgres instance with a named volume for persistence. The pgdata volume survives container restarts and recreation, so your data isn’t lost when you run docker compose down (without the -v flag).
Understanding the Volume Mapping
The line pgdata:/var/lib/postgresql/data maps a Docker-managed named volume to Postgres’s internal data directory. This is the single most important line in the file — without it, every docker compose down followed by up wipes your database clean.
There are two common approaches:
pgdata:/var/lib/postgresql/data as shown above../data:/var/lib/postgresql/data. This gives you direct filesystem access but can run into UID/GID permission mismatches between your host user and the container’s postgres user (UID 999 in the official image).For most setups, named volumes are simpler and less error-prone. Reserve bind mounts for cases where you need to inspect or back up raw data files directly from the host.
Environment Variables and Secrets
Hardcoding passwords in docker-compose.yml is a common mistake that gets committed to git accidentally. Instead, use a .env file:
# .env
POSTGRES_USER=appuser
POSTGRES_PASSWORD=a-much-stronger-password-here
POSTGRES_DB=appdb
Reference it in your Compose file:
services:
db:
image: postgres:16
restart: unless-stopped
env_file:
- .env
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Add .env to your .gitignore immediately:
echo ".env" >> .gitignore
For production deployments, consider Docker secrets or a dedicated secrets manager instead of plain .env files — environment variables are visible to anything that can inspect the running container (docker inspect), which is a real exposure risk on shared hosts.
Adding Health Checks and a Custom Network
A production-grade Compose file should confirm Postgres is actually accepting connections before dependent services (like your app) try to connect. Add a health check:
services:
db:
image: postgres:16
restart: unless-stopped
env_file:
- .env
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
app:
build: .
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
networks:
- backend
volumes:
pgdata:
networks:
backend:
The depends_on.condition: service_healthy clause ensures your app container waits for Postgres to actually be ready, not just started. This eliminates the classic “connection refused” error on the first boot of a multi-container stack.
Notice the app connects to db, not localhost or 127.0.0.1. Compose automatically creates a DNS entry for each service name on the shared network, so containers reach each other by service name.
Backups and Data Recovery
Even with persistent volumes, you need actual backups — a corrupted volume or accidental docker compose down -v can still destroy your data. Use pg_dump inside the running container:
docker compose exec db pg_dump -U appuser appdb > backup.sql
Restore into a fresh container:
cat backup.sql | docker compose exec -T db psql -U appuser -d appdb
For automated, scheduled backups, add a lightweight sidecar service using a cron-based image, or run pg_dump from a host cron job that shells into the container. If you’re managing this on a remote VPS, our Linux server hardening checklist covers securing SSH and cron jobs so scheduled backup scripts aren’t a security liability.
For larger datasets, consider pg_basebackup for physical backups, which are faster to restore than logical pg_dump exports. The official PostgreSQL backup documentation covers the tradeoffs between logical and physical backup strategies in detail.
Running Multiple Postgres Versions Side by Side
One underrated benefit of Docker Compose is testing against multiple Postgres major versions without touching your host system. Just change the image tag per project:
services:
db-15:
image: postgres:15
ports:
- "5433:5432"
volumes:
- pgdata15:/var/lib/postgresql/data
db-16:
image: postgres:16
ports:
- "5434:5432"
volumes:
- pgdata16:/var/lib/postgresql/data
volumes:
pgdata15:
pgdata16:
This is invaluable when validating an upgrade path before touching production. Spin up both versions, dump data from the old one, restore into the new one, and confirm your app behaves identically before committing to the upgrade.
Choosing Where to Host Your Postgres + Compose Stack
Once your Compose setup works locally, you’ll want to deploy it to a real server. A small VPS with a few gigabytes of RAM handles most small-to-medium Postgres workloads comfortably. Providers like DigitalOcean and Hetzner both offer affordable droplets/instances that run Docker Compose stacks well out of the box — DigitalOcean’s managed load balancers and snapshots are convenient if you want less manual ops work, while Hetzner tends to offer more raw compute per dollar if you’re comfortable managing more yourself.
Whichever you choose, make sure to configure a firewall so port 5432 isn’t exposed to the public internet — bind it to 127.0.0.1:5432:5432 instead of 5432:5432 if only local services need access, and put Postgres behind a private network or SSH tunnel for remote administration.
Recommended: Want to explore DigitalOcean yourself? DigitalOcean is a direct vendor link (not an affiliate/tracked link).
FAQ
Does Docker Compose persist Postgres data by default?
No. Without an explicit volume mapping, data is stored inside the container’s writable layer and is lost when the container is removed. Always define a named volume or bind mount for /var/lib/postgresql/data.
What’s the difference between docker compose down and docker compose down -v?
docker compose down stops and removes containers and networks but leaves named volumes intact. Adding -v also deletes the volumes, which permanently erases your database data — use it carefully.
Can I run pgAdmin alongside Postgres in the same Compose file?
Yes. Add a pgadmin service using the dpage/pgadmin4 image on the same network as your db service, then connect using the service name db as the host in pgAdmin’s connection settings.
Why does my app container fail to connect to Postgres on startup?
This usually means the app tried to connect before Postgres finished initializing. Add a healthcheck to the db service and use depends_on.condition: service_healthy on the app service to fix the race condition.
How do I change the Postgres password after the container has already initialized?
Environment variables like POSTGRES_PASSWORD only apply on first initialization of an empty data directory. To change the password afterward, connect with psql and run ALTER USER appuser WITH PASSWORD 'newpassword'; directly.
Is it safe to expose port 5432 publicly?
Generally no. Bind the port to localhost only, or better, don’t publish it at all and let other containers reach Postgres over the internal Compose network by service name.
Wrapping Up
A well-structured docker-compose.yml turns Postgres from a fragile local install into a portable, version-controlled piece of infrastructure. Start with the minimal setup, add named volumes early, move secrets into a .env file, and layer on health checks once you’re connecting other services. From there, backups and a hardened network configuration are the last steps between a dev setup and something you’d trust in production.
If you’re scaling this stack beyond a single server, revisit our Docker networking guide for multi-host and reverse proxy patterns that build directly on the concepts covered here.
Leave a Reply