PostgreSQL 14 End of Life: How to Upgrade to 18 in Docker
PostgreSQL 14 gets its last update on November 12, 2026. The project's versioning policy lists that as the end-of-life date, and the minor release scheduled for the same day will be the final 14.x. Once it ships, no new fixes reach 14, security ones included.
Your database won't stop working on November 13. It just stops getting patches, and every month that passes makes the eventual upgrade a little more awkward. So here's how to move off 14 in a Docker setup: what to check first, the two ways to do it, and the errors you'll most likely hit.
Pick Your Target: 17 or 18
PostgreSQL 19 is still a release candidate, so skip it for production. That leaves two good options:
| Version | Supported until | Pick it if |
|---|---|---|
| 17 | November 8, 2029 | You want the more established release and fewer changes at once |
| 18 (18.6 today) | November 14, 2030 | You want the longest runway from this one migration |
My take: go to 18. You're doing the work anyway, and it buys you an extra year. You can jump straight from 14 to 18 with no stop in between. The rest of this guide assumes 18, and I'll point out where 18 adds a gotcha that 17 doesn't.
Before You Touch Anything
Run these against your 14 database. Replace postgres with the service name and user from your docker-compose.yml.
docker compose exec -T postgres psql -U postgres -c "SELECT version();"
docker compose exec -T postgres psql -U postgres -c "SELECT datname, pg_size_pretty(pg_database_size(datname)) FROM pg_database;"
docker compose exec -T postgres psql -U postgres -c "SHOW data_checksums;"
docker compose exec -T postgres psql -U postgres -c "SELECT rolname FROM pg_authid WHERE rolpassword LIKE 'md5%';"
Here's what each answer tells you:
- Size decides the method. A few gigabytes dumps and restores in minutes. Hundreds of gigabytes is where
pg_upgradestarts to pay off. - Checksums matter for
pg_upgradeto 18, because 18 turns checksums on by default andpg_upgradeneeds both sides to match. Clusters created by the official Docker image before 18 usually sayoff. - MD5 passwords still work in 18, but they're deprecated and will be removed in a future release. Any roles listed should move to SCRAM soon.
Also list your extensions in each database with \dx. Every one of them has to be available for 18 in the new image, so a custom image with PostGIS or pgvector needs an 18 build first.
Method 1: Dump and Restore (Start Here)
This is the boring option, and boring is good here. Your 14 data stays untouched the whole time, so rolling back is just starting the old container again.
1. Stop your app, so nothing writes during the dump. Then dump everything, roles included:
docker compose exec -T postgres pg_dumpall -U postgres > dump-14.sql
Check that the file isn't empty and ends with PostgreSQL database cluster dump complete.
2. Point the service at 18, with a new volume path. This is the part people miss. From 18 on, the official image keeps its data under /var/lib/postgresql/18/docker and expects the volume at /var/lib/postgresql, not at /var/lib/postgresql/data like 14 did. Give it a fresh folder, and leave the old one alone:
services:
postgres:
image: postgres:18
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./pgdata-18:/var/lib/postgresql
3. Start the new server and restore:
docker compose up -d postgres
docker compose exec -T postgres psql -U postgres -f - < dump-14.sql
You'll see role "postgres" already exists. That's expected, since the new cluster already created it. Watch for any other errors.
4. Refresh statistics before your app comes back. A restored database has no planner statistics, so the first queries can be slow:
docker compose exec -T postgres vacuumdb -U postgres --all --analyze-in-stages
Start your app, test it, and keep ./data and dump-14.sql around for a week or so.
Method 2: pg_upgrade (When the Dump Is Too Slow)
For big databases, pg_upgrade converts the data files instead of replaying SQL, and with --link it can finish in minutes. The catch in Docker is that it needs the binaries for both versions in one container, and the official images only ship one.
The community image tianon/postgres-upgrade has both, with a 14-to-18 tag. Its README calls it a proof of concept to adapt, not something to run blindly, and that's fair advice. Take a full backup first, and practice on a copy.
Two things to get right:
- Checksums. If your 14 cluster reported
off, the new 18 cluster must be created with--no-data-checksums, orpg_upgraderefuses to continue. - Afterwards, run the same
vacuumdb --analyze-in-stagesas above. If you use full-text search orpg_trgm, the 18 release notes recommend reindexing those indexes after apg_upgrade.
If that sounds like more ceremony than you want, the dump route is fine for most small and medium databases. Time a test dump to find out where yours falls.
Doing It on Elestio
PostgreSQL on Elestio starts at $11/month for the VM, with automated backups included. Don't just switch an existing 14 service to 18 with Change version. Data files from one major version can't be opened by another, so PostgreSQL refuses to start.
You have two safe options:
- Do it yourself, side by side. Deploy a new PostgreSQL service on 18, then follow Method 1, dumping from the old service and restoring into the new one. Point your app at the new service and delete the old one when you're happy. The Clone button in your dashboard is also a cheap way to rehearse on a copy of the 14 service first.
- Let AI DevOps handle it. Elestio AI DevOps can run an in-place major upgrade on the running service. It takes a backup before it touches anything, and you watch every command live.
The Parts That Changed Since 14
A few defaults changed between 14 and 18. Most won't affect you, but these are worth knowing:
- Public schema permissions (15). New databases no longer let every user create objects in
public. Upgrades and restores keep your existing permissions, but a fresh database created on 18 won't, and that can break app setup scripts. - Generated columns (18) are now virtual by default. Add
STOREDif your migrations relied on the old behavior. - Exclusive backup mode (15) is gone. Old scripts calling
pg_start_backup()needpg_backup_start().
Read the release notes for 15, 16, 17 and 18 if you use anything unusual. The "Migration" section of each is short.
Troubleshooting
Error: in 18+, these Docker images are configured to store database data in a... You kept the old /var/lib/postgresql/data mount. Mount your new volume at /var/lib/postgresql instead, as in step 2.
database files are incompatible with server. You pointed an 18 image at a 14 data directory. Switch back to postgres:14 and do the upgrade properly.
pg_upgrade stops on a checksum mismatch. Recreate the new cluster with --no-data-checksums to match your 14 cluster.
Restore fails on an extension. The 18 image doesn't have it. Use an image that includes it, then restore again into a fresh volume.
Everything's slow after the upgrade. You skipped the statistics step. Run vacuumdb --all --analyze-in-stages.
Warnings when changing passwords. Those are the MD5 deprecation warnings. Set password_encryption = 'scram-sha-256' and reset those roles' passwords.
Thanks for reading ❤️ See you in the next one 👋