How to Upgrade Postgres Major Versions in Docker Compose Without Losing Data
Handle major Postgres version jumps in Docker Compose with a safer plan that checks compatibility first, uses a verified backup path, and keeps the cutover separate from blind container replacement.
How to choose between dump-and-restore and side-by-side upgrade patterns, validate the new database, and cut your app over with a realistic rollback boundary.
Single-VPS Compose stacks where Postgres is private to the app and the operator wants a safe path, not the fastest possible one.
The most dangerous mistake is changing the Postgres image tag and reusing a live data directory as if a major upgrade were the same as a normal container refresh.
Before you begin
- Know your current Postgres major version and the target version.
- Know which app version, extensions, and client libraries must remain compatible with the target version.
- Have enough disk space for a second copy of the data or for dump files during the upgrade.
This guide assumes Postgres runs inside a Docker Compose project and is private to the app. The safest default for many small operators is not an in-place binary upgrade. It is a fresh target database plus verified restore. That path is slower, but easier to reason about and easier to unwind before cutover.
Step 1: Choose the upgrade strategy
For most self-hosted Compose stacks, use one of these patterns:
- Dump and restore: safest and easiest to explain. Export from the old version, create a fresh target volume, restore into the new version, then cut over.
- Side-by-side logical replication or migration: useful for larger or more availability-sensitive setups, but more moving parts.
pg_upgradestyle binary upgrade: faster for experienced operators, but much easier to get wrong in small ad hoc Compose environments.
If you are unsure, choose dump and restore. The goal is to preserve data and reduce surprises, not to minimize downtime at all costs.
image: postgres:X to a new major version against the same live volume and hope the container upgrades it for you. Major version upgrades are deliberate data operations.Step 2: Check compatibility before touching data
Confirm the app supports the target Postgres major version. Then check extension usage, client drivers, and any app-specific migration requirements. If the app vendor or project says “upgrade the app first” or “upgrade Postgres first,” follow that contract.
Also review database size, downtime tolerance, and whether you need to pause writes during export. Large or very active databases may need a stricter maintenance window than small hobby stacks.
Step 3: Take a verified backup you can actually use
Take a logical backup before you start, even if you also have volume snapshots. A dump is portable across clean target volumes and easier to inspect. For example:
# Example logical backup from the running old database
docker compose exec db pg_dumpall -U postgres > postgres-before-major-upgrade.sql
# Optional compression
gzip -9 postgres-before-major-upgrade.sql
If the database is large, you may prefer per-database dumps, but the principle stays the same: create a portable export, store it safely, and make sure it is readable before moving on.
Step 4: Build the target database on a clean volume
Create a separate Compose service definition or temporary override for the target Postgres major version using a new volume or new bind-mounted data path. Keep the old database untouched until cutover is complete. This gives you a real fallback.
# Example idea, not a copy-paste universal file
services:
db_new:
image: postgres:17
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres17_data:/var/lib/postgresql/data
Start the new database cleanly, confirm it initializes, and only then import data into it.
Step 5: Restore into the new database and validate
Import the dump into the clean target database, then verify databases, roles, extensions, and basic app queries.
# Example restore flow
gunzip -c postgres-before-major-upgrade.sql.gz | docker compose exec -T db_new psql -U postgres
After the restore, check that expected databases exist, extension creation succeeded, and the application can connect in a test path without pointing production traffic at it yet. This is also the right time to run app migrations if the application expects them after the Postgres version change.
Step 6: Cut over the app carefully
Once the target database is validated, put the app into maintenance mode or pause writes, repoint the app to the new database service or volume, and restart only the app components that need the connection. Then test the real write path. If the app has workers, bring them back only after the web path is confirmed healthy.
Keep the old database stopped but intact until you are confident the new one is stable.
Step 7: Understand the rollback boundary
Rollback is easy only before new writes land on the upgraded database. Once production begins writing to the new version, reverting to the old database means choosing how to reconcile new data. That is why the safest moment to back out is before reopening traffic. After reopen, treat rollback as a separate data decision, not a casual container restart.
Expected outcomes
- You have a verified logical backup from before the upgrade.
- The target Postgres major version runs on a clean, separate data path.
- The app connects and passes real checks before traffic returns.
- The old database remains available as a pre-cutover fallback until confidence is high.
Troubleshooting
The new container starts but the restore fails: check client and server version assumptions, extension availability, and whether the dump was produced cleanly.
The app connects but migrations or writes fail: the app version, extension set, or SQL assumptions may not match the target Postgres version yet.
You ran the new image against the old volume already: stop, preserve evidence, and do not keep retrying random image tags. Inspect logs and verify whether a safe restore from the logical backup is cleaner.
Rollback is suddenly unclear: if traffic has not reopened, return to the old database immediately. If it has, stop and decide how to handle post-cutover writes before pretending rollback is trivial.
What to do next
Once your database upgrade path is safe, the next maturity step is rehearsing the app in isolation before production changes. Continue with How to Clone a Production Self-Hosted App Into a Safe Staging Environment.
