Postgres Docker Compose: The Complete Setup Guide
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.
Quick answer: run Postgres safely with Compose
Define the Postgres image, credentials, persistent volume, and healthcheck in Compose. Use the healthcheck before starting dependent services, and verify the volume before changing or removing the stack.
docker compose up -d dbndocker compose psndocker compose logs --tail=100 dbWhy 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 versionIf 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.ymlAdd 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 -dVerify the container is running and healthy:
docker compose ps
docker compose logs dbConnect using psql from your host (if installed) or directly inside the container:
docker compose exec db psql -U appuser -d appdbThat’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=appdbReference 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" >> .gitignoreFor 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.sqlRestore into a fresh container:
cat backup.sql | docker compose exec -T db psql -U appuser -d appdbFor 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.
Related Docker Compose guides
Continue with Docker Compose volumes, Docker Compose logs, or docker compose down.
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.
Networking Between Services
By default, Compose creates a private network for all services defined in the same file. The app service above reaches PostgreSQL simply by using db as the hostname — Docker’s internal DNS resolves it automatically. You don’t need to expose port 5432 to the host at all unless you want external tools (like a local GUI client) to connect directly.
If you do need host access for debugging with a tool like pgAdmin or DBeaver, keep the port mapping but restrict it:
ports:
- "127.0.0.1:5432:5432"Binding to 127.0.0.1 instead of 0.0.0.0 prevents the database port from being exposed on your machine’s public network interface — a small change that matters a lot if you’re running this on a cloud VPS rather than a laptop.
Multi-Container Example with pgAdmin
For local development, it’s often convenient to run a web-based admin UI alongside PostgreSQL:
services:
db:
image: postgres:16
restart: unless-stopped
env_file:
- .env
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 10s
timeout: 5s
retries: 5
pgadmin:
image: dpage/pgadmin4:latest
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: [email protected]
PGADMIN_DEFAULT_PASSWORD: changeme
ports:
- "127.0.0.1:8080:80"
depends_on:
- db
volumes:
pgdata:Access pgAdmin at http://localhost:8080, then add a new server connection pointing to host db, port 5432, using the credentials from your .env file.
Production Hardening Checklist
Before you point real traffic at a containerized PostgreSQL instance, review these items:
127.0.0.1 or remove the port mapping entirely if only internal services need access.restart: unless-stopped so the database recovers automatically after a host reboot.postgres:16.3, not just postgres:latest) to avoid surprise upgrades.If you’re hosting this yourself rather than using a managed database, providers like Hetzner offer solid price-to-performance ratios for dedicated database VPS instances, which matters since PostgreSQL performance is heavily tied to disk I/O and available RAM for caching.
Resource Limits
Compose lets you cap CPU and memory so a runaway query doesn’t starve other containers on the same host:
services:
db:
image: postgres:16
deploy:
resources:
limits:
cpus: "2"
memory: 2GNote that deploy.resources is fully honored under Docker Swarm; under plain docker compose up, Compose v2 does apply these limits on recent Docker Engine versions, but it’s worth verifying with docker stats after deployment rather than assuming it’s enforced.
Common Errors and Fixes
A few issues come up repeatedly when people first containerize PostgreSQL:
POSTGRES_USER env var wasn’t set before the volume was initialized for the first time. Environment variables only take effect on first container creation; changing them later has no effect on an existing volume.docker compose down — someone ran docker compose down -v and deleted the volume along with the containers.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.
Related articles:
- Docker Compose Down: Full Guide to Stopping Stacks
- Docker Compose Build: Complete Guide to Building Images
- Docker Compose Env: Manage Variables the Right Way
- see our article on docker Compose Logs: The Complete Debugging Guide
- see our article on docker Compose Log Command: A Complete Debugging Guide
Related: this container setup guide.
Related: Cloudflare configuration.