Docker Compose Postgres: A Complete Setup and Operations 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 a PostgreSQL instance with Docker Compose is one of the fastest ways to get a reliable, reproducible database environment for local development, staging, or a small production workload. This guide walks through a real docker compose postgres configuration, covering persistence, environment variables, networking, backups, and common troubleshooting steps so you can run Postgres in containers with confidence.
Postgres is one of the most widely deployed relational databases, and Docker Compose makes it trivial to spin up a consistent instance across machines without touching a host-level package manager. Whether you’re prototyping an application, running CI tests, or managing a small self-hosted stack, a docker compose postgres setup gives you version-pinned, disposable infrastructure that behaves the same way everywhere.
Why Use Docker Compose for Postgres
Before diving into configuration, it helps to understand why so many teams choose a docker compose postgres approach over installing Postgres directly on the host.
A containerized database gives you:
docker-compose.yml works on a laptop, a CI runner, or a VPSThis isn’t a claim that containers are inherently faster or “the best” way to run a database — it’s a practical tradeoff. For small-to-medium workloads and development environments, the operational simplicity is usually worth it. For very large, latency-sensitive production databases, a managed service or dedicated host may be more appropriate.
Basic Docker Compose Postgres Configuration
The minimum viable docker compose postgres setup only needs a handful of fields: an image, a set of environment variables, and a persistent volume.
version: "3.9"
services:
postgres:
image: postgres:16
container_name: postgres_db
restart: unless-stopped
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: change_this_password
POSTGRES_DB: appdb
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
Run it with:
docker compose up -d
This starts a single Postgres container, exposes port 5432 to the host, and stores data in a named Docker volume so it survives container restarts and recreation.
Choosing the Right Postgres Image Tag
Always pin your image to a specific major version (postgres:16, not postgres:latest). An unpinned tag means a future docker compose pull could silently move you to a new major version with a different on-disk data format, which Postgres does not support upgrading in place without a dump-and-restore or a dedicated upgrade tool. Pinning avoids that surprise entirely.
Environment Variables That Matter
The official Postgres image reads several environment variables at first-run time only — they are applied when the data directory is initialized, not on every subsequent start:
POSTGRES_USER — the superuser role created on first bootPOSTGRES_PASSWORD — required unless you explicitly opt into trust authenticationPOSTGRES_DB — an initial database created alongside the userPGDATA — override the internal data directory path if neededIf you need to manage many environment values across services, see this guide on how to manage Docker Compose environment variables cleanly using .env files instead of hardcoding secrets into the compose file.
Persisting Data With Volumes
The single most important part of any docker compose postgres deployment is making sure data survives a container restart, rebuild, or host reboot. Without a volume, deleting the container deletes your database.
There are two common approaches:
Named Volumes
Named volumes (as shown in the example above) are managed by Docker itself and stored under /var/lib/docker/volumes/. They’re the recommended default because Docker handles permissions and lifecycle for you, and they work identically across Linux, macOS, and Windows hosts.
Bind Mounts
A bind mount maps a specific host directory into the container:
volumes:
- ./pgdata:/var/lib/postgresql/data
Bind mounts are useful when you want direct filesystem access to the data files from the host — for example, to inspect them with host-level backup tooling — but they’re more sensitive to permission mismatches between the host user and the container’s postgres user. For most setups, a named volume is the simpler and more portable choice.
Networking and Connecting to Postgres From Other Containers
A docker compose postgres service running alongside your application doesn’t need its port published to the host at all if only other containers need to reach it. Compose automatically creates a shared network where services can resolve each other by service name.
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: change_this_password
POSTGRES_DB: appdb
volumes:
- postgres_data:/var/lib/postgresql/data
app:
build: .
environment:
DATABASE_URL: postgresql://appuser:change_this_password@postgres:5432/appdb
depends_on:
- postgres
volumes:
postgres_data:
Here, the app service connects to postgres:5432 using the service name as the hostname — no port needs to be exposed externally unless you want to connect from a local client like psql or a GUI tool on the host machine.
Using depends_on and Healthchecks Correctly
depends_on only controls container start order, not database readiness. Postgres can still be initializing when your application container starts, causing early connection failures. A healthcheck fixes this:
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: change_this_password
POSTGRES_DB: appdb
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 5s
timeout: 5s
retries: 5
app:
build: .
depends_on:
postgres:
condition: service_healthy
volumes:
postgres_data:
Now app waits until Postgres actually reports itself ready via pg_isready, not just until the container process has started.
Backups and Data Safety
Running Postgres in Docker doesn’t change the fundamentals of database backup strategy — you still need a repeatable, tested backup process, container or not.
Manual Dumps With pg_dump
The simplest backup approach is running pg_dump inside the running container:
docker compose exec postgres pg_dump -U appuser appdb > backup.sql
Restoring is the reverse:
cat backup.sql | docker compose exec -T postgres psql -U appuser -d appdb
Automating Backups
For anything beyond occasional manual dumps, schedule this command via cron or a dedicated backup sidecar container, and store the resulting archive somewhere outside the Docker host — object storage or a separate backup volume. A backup that only lives on the same disk as the database it protects doesn’t protect against disk failure.
If you’re running Postgres as part of a larger automation stack, it’s also worth reviewing how Docker Compose volumes behave under different backup and snapshot strategies, since volume management directly affects how safely you can restore state.
Troubleshooting Common Docker Compose Postgres Issues
Even a straightforward docker compose postgres setup runs into a handful of recurring problems. A few of the most common:
Container Restarts in a Loop
If the Postgres container keeps restarting, check the logs first:
docker compose logs postgres
For a deeper look at filtering and following logs across a multi-service stack, this Docker Compose logs debugging guide covers flags and patterns that make diagnosing startup failures much faster.
A common cause is a data directory that was initialized with a different Postgres major version than the image now specifies — Postgres refuses to start against an incompatible on-disk format. The fix is either reverting the image tag or performing a proper version upgrade (dump on the old version, restore on the new one).
“Connection Refused” From the Application Container
This almost always means one of three things: the healthcheck/depends_on gating isn’t in place yet, the application is trying to connect to localhost instead of the service name (postgres in the examples above), or the password/user doesn’t match what was set on first initialization (remember, POSTGRES_PASSWORD is only applied on the very first container start — changing it later in the compose file has no effect on an existing volume).
Environment Variables Not Taking Effect
Because Postgres only reads POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB during first-time initialization, changing them after the volume already contains data does nothing. To apply new values, you either need to run the appropriate ALTER ROLE/CREATE DATABASE SQL manually, or remove the volume and reinitialize (losing existing data unless backed up first).
Comparing Docker Compose Postgres to a Managed Database
It’s worth being honest about tradeoffs. A docker compose postgres setup gives you full control and no vendor lock-in, but you’re also responsible for patching, backups, high availability, and monitoring yourself. A managed database service handles most of that operational burden for you, at the cost of less control and ongoing hosting fees.
For development, CI, and small self-hosted production workloads — the kind commonly deployed on a single VPS — running Postgres via Docker Compose is a solid, well-understood approach. If you’re deploying this alongside other self-hosted services on a VPS, providers like DigitalOcean or Hetzner offer straightforward virtual machines that work well for this kind of Docker-based stack.
If you’re also comparing container orchestration options for running Postgres at larger scale, this Kubernetes vs Docker Compose comparison walks through when it makes sense to graduate beyond a single-host Compose setup.
Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.
FAQ
Does Docker Compose Postgres data persist after docker compose down?
Yes, as long as you’re using a named volume or bind mount (not an anonymous volume). docker compose down removes containers and the default network but leaves named volumes intact. Data is only lost if you run docker compose down -v, which explicitly removes volumes too.
How do I change the Postgres password after the container has already initialized?
Environment variables like POSTGRES_PASSWORD only apply on first initialization. To change the password afterward, connect with psql and run ALTER ROLE appuser WITH PASSWORD 'new_password'; directly against the running database.
Can I run multiple Postgres containers in the same Docker Compose Postgres project?
Yes. Give each service a distinct name, a distinct volume, and either distinct host ports (if you need external access to both) or none at all if they’re only reached by other containers over the internal network.
Why does my docker compose postgres container fail to start after a Postgres version upgrade in the image tag?
Postgres data files are not forward-compatible in place across major versions. You need to either keep the image tag matching the version the volume was initialized with, or perform a proper dump-and-restore migration to the new version.
Conclusion
A docker compose postgres setup gives you a reproducible, version-pinned database environment with minimal configuration overhead. The core requirements are simple — persist data with a named volume, pin your image tag, gate application startup on a real healthcheck rather than just container start order, and maintain a tested backup process outside the container itself. Get those fundamentals right, and Docker Compose is a dependable way to run Postgres for development, CI, and many production workloads. For further reading on the underlying tooling, the official Docker documentation and the PostgreSQL documentation are the most reliable references for configuration options not covered here.
Leave a Reply