Guide
How do you upgrade Postgres to a new major version in Docker Compose?
You cannot change `postgres:14` to `postgres:18` and restart. Dump the old cluster, start 18 on a fresh mount at `/var/lib/postgresql`, and restore. Postgres 14 stops getting fixes on 12 November 2026.
How do you upgrade Postgres to a new major version in Docker Compose?
Not by changing the image tag: a newer major refuses to start on an older data directory. Stop the app, run pg_dumpall against the old container, bring the stack down, point the database service at the new image with an empty, new volume (for postgres:18 and later, mounted at /var/lib/postgresql, not /var/lib/postgresql/data), restore the dump with psql, then start the app. Keep the old volume until the app has run cleanly for a week. pg_upgrade is faster but fails from a cluster without data checksums to a default Postgres 18 cluster, which has them on.
Not by changing the image tag. Dump the old cluster with pg_dumpall, start the new major on an empty volume, restore the dump, then start the app. For postgres:18 and later the new volume goes at /var/lib/postgresql, not at /var/lib/postgresql/data where every older compose file mounts it, and that one line is where most first attempts fail. The deadline that makes this urgent: Postgres 14, still the version in Immich's compose file, gets its last fix on 12 November 2026.
Why changing the tag does not work#
PostgreSQL's documentation is plain about the cause: for major releases, "the internal data storage format is subject to change". Minor releases never change it, which is why postgres:16.14 to 16.15 is a pull and a restart. A major is not. Point a 17 server at a 14 data directory and it stops at startup with:
FATAL: database files are incompatible with server
DETAIL: The data directory was initialized by PostgreSQL version 14, which is not compatible with this version 17.11.The official image for 18 fails differently, and earlier. Its PGDATA moved to /var/lib/postgresql/18/docker and its VOLUME to /var/lib/postgresql. If the 18 entrypoint finds an empty PGDATA but a database at /var/lib/postgresql/data, it refuses to initialise and prints Error: in 18+, these Docker images are configured to store database data in a format which is compatible with "pg_ctlcluster", followed by the explanation that this "is usually the result of upgrading the Docker image without upgrading the underlying database". It does the same if anything at all is mounted at /var/lib/postgresql/data, even an empty volume.
Both failures are safe. The data is untouched; put the old tag back and the stack starts. The confusion is that most compose files people copied were written before 18, so the obvious edit, changing 16 to 18, hits the second error and looks like a broken image.
Which apps are on which major#
The version you must leave and the mount path you have come from the compose file each project tells you to start from. Read on 11 October 2026:
| App | Ships | Mounted at | Checksums | What to do |
|---|---|---|---|---|
| Immich 3.3.1 | ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 | /var/lib/postgresql/data | On (--data-checksums in the compose file) | Stay on Immich's image; see below |
| authentik 2026.8.3 | postgres:16-alpine | /var/lib/postgresql/data | Off (image default before 18) | No hurry; 16 is supported to November 2028 |
| Paperless-ngx | postgres:18 | /var/lib/postgresql | On (18 default) | Already current |
| Mattermost | postgres:18-alpine | /var/lib/postgresql | On (18 default) | Already current; older installs on 14 must move before v12.0 |
| Synapse | No image pinned | Yours | Yours | Off 14 before support is withdrawn after 12 November |
And the dates that decide how long each major is safe, from the PostgreSQL project's versioning page:
| Major | Current minor | End of life |
|---|---|---|
| 14 | 14.24 | 12 November 2026 |
| 15 | 15.19 | 11 November 2027 |
| 16 | 16.15 | 9 November 2028 |
| 17 | 17.11 | 8 November 2029 |
| 18 | 18.6 | 14 November 2030 |
End of life does not stop a server. It means one final minor release and then nothing. The pressure comes from the apps. Synapse's policy is that when a Postgres version reaches end of life it "will withdraw support for that version in future releases". Mattermost's published schedule moves the minimum to 15 at v12.0, dated 14 October 2026, and its policy says customers "will have 9 months to plan, test, and upgrade" before each new minimum. If you are on 14 and run Mattermost's feature releases, you are already late.
Dump and restore, or pg_upgrade?#
PostgreSQL documents three ways across a major: dump and restore with pg_dumpall, an in-place migration with pg_upgrade, and logical replication. For a self-hosted app the choice is between the first two.
Dump and restore is the one to use unless your database is too big to sit offline while it reloads. It works across any number of majors, it does not care about checksums or extensions beyond having them installed, and the old volume is never written to, so rolling back is putting the old tag back. It is also the procedure authentik documents for its own upgrades.
pg_upgrade is faster, and with --link it barely copies anything. It needs both versions' binaries in one place, which the official images do not provide, and it is strict: the documentation warns that in link mode "you will not be able to access your old cluster once you start the new cluster". It is the right tool when the database is too large to reload in your maintenance window. For most home databases, its speed is not worth the extra ways it can fail.
The upgrade, step by step#
This assumes a compose service called db, a superuser called app, and an app service called server. Replace the names with yours; they are in your compose file and .env.
1. Record what is running. Ask the server, not the file:
docker compose exec db psql -U app -d postgres -Atc 'SHOW server_version' -c 'SHOW data_checksums'2. Stop the app, leave the database up, so nothing writes during the dump:
docker compose stop server3. Dump the whole cluster. pg_dumpall includes roles, which pg_dump does not; Backing up a running database covers the difference.
docker compose exec -T db pg_dumpall -U app > pg-$(date +%F).sql
grep -c 'cluster dump complete' pg-$(date +%F).sql # 1 means the dump finishedPostgreSQL recommends dumping with the newer version's pg_dumpall where you can, "to take advantage of enhancements". Running the old container's own tool also works, and it is what authentik's guide does.
4. Bring the stack down and change two lines. Do not reuse the old volume. A new one leaves the old data exactly as it was:
services:
db:
image: postgres:18 # was postgres:16-alpine
volumes:
- pgdata18:/var/lib/postgresql # was pgdata:/var/lib/postgresql/data
volumes:
pgdata: # keep it until you are sure
pgdata18:5. Start only the database, then restore:
docker compose down
docker compose up -d db
docker compose exec -T db psql -U app -d postgres < pg-$(date +%F).sql
docker compose exec db vacuumdb -U app --all --analyze-onlyExpect two harmless errors near the top, role "app" already exists and database "app" already exists, because the image created both from POSTGRES_USER and POSTGRES_DB on first start. Any other error is not harmless; stop and read it.
6. Start the app and use it. docker compose up -d, log in, open something old and something new. Leave pgdata in place for a week, and keep the dump. That is your rollback: the old tag, the old volume name, up -d.
The checksum trap, if you use pg_upgrade anyway#
If the database is large enough that dump and restore is not an option, the tianon/postgres-upgrade images package both versions' binaries as tags such as 16-to-18, and pgautoupgrade wraps the same idea into an image that upgrades whatever it finds at startup. Both run into one change in Postgres 18 that older guides predate.
Postgres 18's initdb enables data checksums by default. Every earlier version defaulted to off, so a cluster created by postgres:16 or postgres:14 with default settings has none. pg_upgrade requires both clusters to match, and its source has no way round it:
old cluster does not use data checksums but the new one doesRun SHOW data_checksums on the old server first. If it says off, the new cluster has to be created with --no-data-checksums; tianon's image initialises the new cluster with whatever is in POSTGRES_INITDB_ARGS, so set it to that. If it says on, as it will for Immich's database, the default matches. Two more things change at 18: pg_upgrade now carries most optimizer statistics across, and the documentation's post-upgrade step is vacuumdb --all --analyze-in-stages --missing-stats-only followed by vacuumdb --all --analyze-only.
Treat both tools as what they say they are. tianon's README calls itself a proof of concept: "don't expect it to work as-is". pgautoupgrade upgrades in place with --link, removes the old cluster data, and opens its README with "Backup your data!". Take the dump from step 3 either way.
Immich: leave it on Immich's image#
Immich's database is not plain Postgres. It needs the VectorChord extension preloaded, which is why it runs ghcr.io/immich-app/postgres rather than the official image, and why sharing its server is a bad idea. Immich publishes that image for 15, 16, 17 and 18, but the 18 tags carry VectorChord 0.5.3 or 1.1.1, not the 0.4.3 its compose file pins, so moving Immich to 18 changes the extension version in the same step. Immich's upgrade documentation gives no procedure for a Postgres major.
So the honest recommendation is to wait for Immich to move its compose file, and to be clear about the cost. From 12 November its database runs an unsupported Postgres. The reference compose file publishes no port for it, so it is reachable only from Immich's own containers, which makes that a smaller risk than an unpatched service on your network. When you do move it, use Immich's own backup and restore procedure, which includes a sed line that rewrites the dump's search_path, rather than the generic one above.
When this answer stops applying#
- Not the official image. Bitnami, Immich's image and others set their own paths and users. The 18 path change and the entrypoint error are specific to the official
postgresimage. - A shared server. One
pg_dumpallmoves every app's database at once, so every app has to support the target major. The per-app version table is the check. - Databases too large to reload in your maintenance window. Then
pg_upgradeis the right tool, with the checksum setting matched and a tested backup. - The table moves. Every row reflects the compose files as read on 11 October 2026.
What to do next#
Run the version check from step 1 against every Postgres container you have, and list any on 14. Do those first, then anything feeding an app with a published minimum, such as Mattermost or Synapse. Before you start, make sure last night's dump actually restores: Backups that actually restore has the rehearsal, and Backup planner schedules it. Then pin the new major in the compose file, never latest, for the reasons in An update strategy that does not lose data.
Questions#
Can I just change postgres:14 to postgres:18 in my compose file?
No. A major version changes the on-disk format, and the server refuses an older data directory with database files are incompatible with server. The official postgres:18 image stops even earlier: if it finds data at /var/lib/postgresql/data, its entrypoint exits with an error explaining that 18 and later store data in a version-specific directory. Nothing is damaged in either case, but nothing starts either. Put the old tag back and do a dump and restore.
What happens to my database after Postgres 14 end of life?
It keeps running. End of life on 12 November 2026 means the PostgreSQL project releases one final minor version and then no more fixes, security or otherwise. The practical deadline is set by your apps: Synapse withdraws support for a Postgres version once upstream ends it, and Mattermost's schedule requires Postgres 15 from v12.0, dated 14 October 2026. A database with no published port is a lower risk than one reachable from your network.
Should I upgrade from 14 to 15 first, or straight to 18?
Straight to 18. Both pg_dumpall with a restore and pg_upgrade cross several majors in one step, and PostgreSQL's own documentation describes the methods without any requirement to go one major at a time. The constraint comes from the app, not Postgres: check that its documentation or reference compose file lists 18. Paperless-ngx and Mattermost already ship 18; if your app's highest listed version is 16 or 17, go there instead.
Why does pg_upgrade fail with old cluster does not use data checksums but the new one does?
Because Postgres 18's initdb enables data checksums by default and earlier versions did not. A cluster created by postgres:16 with default settings has them off, a new 18 cluster has them on, and pg_upgrade requires the two to match. Initialise the new cluster with --no-data-checksums (with tianon's upgrade image, set POSTGRES_INITDB_ARGS=--no-data-checksums). Immich's compose file sets --data-checksums, so its clusters already match.
Should I use pgautoupgrade?
Only with a backup you have already tested. The pgautoupgrade image detects the old version, runs pg_upgrade --link in place and removes the old cluster data, so if anything goes wrong your copy is the only way back. Its README says so in a warning box, and also tells you to remove your own healthchecks because it ships one. A dump and restore takes longer but leaves the old volume untouched.
How do I check which Postgres version a container is running?
Ask the server, not the compose file: docker compose exec db psql -U app -d postgres -Atc 'SHOW server_version'. The tag in the file can be ahead of what is running if the container was never recreated. The same command with SHOW data_checksums tells you whether the cluster has checksums, which decides how pg_upgrade has to be run. Replace db and app with your service and user names.
Sources#
- PostgreSQL versioning policy and end-of-life dates
- PostgreSQL 18 documentation, upgrading a cluster
- PostgreSQL 18 documentation, pg_upgrade
- PostgreSQL 18.0 release notes (data checksums default, pg_upgrade statistics)
- PostgreSQL source, pg_upgrade checksum check (controldata.c)
- Official postgres image documentation, PGDATA in 18 and later
- Official postgres image, 18 entrypoint (old data directory check)
- tianon/postgres-upgrade README
- pgautoupgrade README
- authentik documentation, upgrading PostgreSQL on Docker Compose
- Immich v3.3.1 reference docker-compose.yml
- Immich documentation, backup and restore
- Mattermost documentation, minimum PostgreSQL support policy
- Synapse deprecation policy
Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.