Infrastructure guide
PostgreSQL Docker Compose: Persistence, Backups, and Safe Major Upgrades
Run PostgreSQL in Compose with a pinned major version, private network, durable data, tested logical backups, and a safe upgrade path.
Published and reviewed by OpenAlt · October 1, 2026

Verdict: a durable postgres docker compose setup needs an explicit major version, a named volume, a private network, and backups you can restore. Compose makes replacement repeatable, but it does not provide recovery or make a major upgrade reversible. Keep the database replaceable, its data separately recoverable, and upgrades directed at a clean target. This is the practical baseline OpenAlt recommends for postgres self hosted deployments.
Table of Contents
- A persistent volume is not a backup and a container is not recovery
- Pinned PostgreSQL major version, private network, volume and secret source
- Compact Compose example with healthcheck and no unnecessary public port
- Safe initialization and application connectivity
- pg_dump versus physical/continuous recovery boundaries
- Isolated restore and validation of rows, extensions, owners and writes
- Safe major upgrades: never attach an old directory to a new major
- Operations, monitoring and rollback checklist
- FAQ
A persistent volume is not a backup and a container is not recovery
A PostgreSQL volume protects data from routine container replacement, but it does not prove that the database can be restored. A container packages a running process; recovery requires a separate copy, a known procedure, and evidence that the copy can be loaded and used after failure.
Treat the volume as the working location, not the safety net. A practical minimum includes:
- a named volume for the active data directory;
- a separate destination for logical or physical backup output;
- documented retention and recovery objectives;
- an isolated restore procedure that is exercised regularly.
Backup output must live outside the active volume’s single failure boundary. Replacement asks whether the service can start again; recovery asks whether the data can be reconstructed and trusted. (Docker PostgreSQL networking and connectivity)
Pinned PostgreSQL major version, private network, volume and secret source
Pin the PostgreSQL major version, keep the database on a private Compose network, mount a named volume, and source the password from a secret rather than embedding it in the Compose file.
Avoid a floating image choice that can change the database major without a planned migration. A major-version change should preserve the source instance and use a prepared target. Let the application connect through the Compose service name so the path remains consistent across restarts while the database stays unpublished to the host.
Keep the secret source separate from ordinary configuration. The Compose definition should describe where the secret is mounted, not place its value in version-controlled text. (Docker PostgreSQL networking and connectivity)
Compact Compose example with healthcheck and no unnecessary public port
Use a named volume, a healthcheck, an internal service name, and no database port unless an approved external client truly needs it. The health gate coordinates startup; it does not replace migrations, retries, backups, or restore practice.
services:
db:
image: postgres:18
restart: unless-stopped
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend
secrets:
- postgres_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
app:
build: .
depends_on:
db:
condition: service_healthy
networks:
- backend
secrets:
postgres_password:
file: ./secrets/postgres_password
volumes:
pgdata:
networks:
backend:
The example intentionally has no ports entry for db. The application reaches PostgreSQL through db:5432 inside the network. Compose supports health conditions that gate dependent service startup, making readiness explicit without equating it with full application availability. (Compose startup order)
Safe initialization and application connectivity
Initialize once, connect through the Compose service name, and make the application tolerate temporary database unavailability. Keep initialization, credentials, schema migrations, and connection retries as separate concerns.
On first deployment, confirm that the intended database name, role, secret source, and volume align before the application writes data. An accidental new volume can look like an empty database while the original data remains elsewhere.
Configure the application to use the service name, not localhost: in Compose, PostgreSQL is a peer service on the private network. Add retries and a clear migration step. The healthcheck should confirm that PostgreSQL is ready for the configured database and role, not that migrations have run, extensions are present, or critical application writes succeed.


pg_dump versus physical/continuous recovery boundaries
Use logical dumps when portability and loading into a newer major or architecture matter; use file-system backups or continuous archiving when the recovery design requires physical recovery or a defined recovery point.
A pg_dump-style logical dump represents database contents in a form that can be reloaded across newer PostgreSQL versions and architectures, making it a natural candidate for a clean-major upgrade when application and extension compatibility is confirmed.
File-system copies are version-specific. They may suit a physical recovery design, but they are not portable directories that can move freely between major versions. Choose the method against the failure you must recover from and document the matching procedure. (PostgreSQL backup and restore, PostgreSQL dump and reload)
Isolated restore and validation of rows, extensions, owners and writes
Restore into an isolated PostgreSQL instance, then validate representative rows, required extensions, object owners, and an actual write before trusting the result.
Use a separate Compose project or volume; never point validation at the production data directory. Restore with the method that created the backup, then inspect both structure and application-critical data:
- Confirm the expected databases and schemas exist.
- Check representative rows and important relationships.
- Verify required extensions are installed and usable.
- Confirm owners and roles match the intended access model.
- Perform a controlled write and read it back.
- Record errors, duration, and manual steps.
The backup tool does not guarantee application correctness. The restore procedure proves whether the result is useful for this deployment. (PostgreSQL dump and reload)
Safe major upgrades: never attach an old directory to a new major
Never point a new PostgreSQL major at an old major’s data directory; treat the directory as version-specific. Preserve the original instance, create a clean target, and move data through a documented compatible path, such as a logical dump and reload when it fits the recovery design.
Use this workflow:
- Pin and preserve the current source image and volume.
- Create a separate target service with the new major.
- Prepare required roles, databases, and extensions.
- Load a logical dump or follow the selected physical method.
- Validate rows, ownership, extensions, and writes.
- Switch application connectivity only after validation.
Rollback means directing the application back to the preserved source instance, not downgrading a new-major directory in place. Coordinate writes during cutover; two independent instances accepting changes create a reconciliation problem that Compose cannot solve. (PostgreSQL dump and reload, PostgreSQL backup methods)
Operations, monitoring and rollback checklist
Operate the database as a recovery system, not merely a running container. Observe health, backup freshness, storage headroom, restore evidence, and application errors. Keep rollback bounded by the preserved old instance and define recovery objectives before changing the major version.
- PostgreSQL major version is explicit and change-controlled.
- The active data directory uses a named volume.
- Backup output is outside the active volume.
- Secret values use a controlled secret source.
- The database is private unless external access is intentional.
- Health status and application connection errors are observable.
- Backup freshness and storage capacity have clear thresholds.
- A restore validates rows, extensions, owners, and writes.
- The old instance remains available during cutover.
- Recovery objectives and rollback steps are documented.
For related OpenAlt guidance, browse the database and spreadsheet category. Use the backup storage calculator for retention planning, the RTO/RPO impact calculator for recovery tradeoffs, and the Grafana Docker Compose guide for operational visibility.
FAQ
Should the database publish port 5432?
Usually no for an internal application. Keep PostgreSQL on the private Compose network and let the application use the database service name. Publish a port only when an approved external client requires it, and treat exposure as an intentional access decision. (Docker PostgreSQL networking and connectivity)
Does a named volume make PostgreSQL safe?
No. A named volume preserves the active data location across routine container replacement, but it remains one working copy. Keep separate backup output, define recovery objectives, and validate an isolated restore.
Can a logical dump support a major upgrade?
Yes, when the target version and application dependencies are compatible with the reload plan. Logical dumps can be reloaded across newer versions and architectures, while file copies remain version-specific. Validate extensions, owners, representative rows, and writes on the clean target. (PostgreSQL dump and reload)
What is the safest rollback for a major upgrade?
Preserve the old instance and data separately, validate the new target before switching application traffic, and return to the old instance if necessary. Do not make a new-major process consume an old-major directory; use a documented backup or reload path instead.