apt install postgresql on Ubuntu 22.04 still installs PostgreSQL 14, and PostgreSQL 14 end of life is 12 November 2026, when the project ships its last minor release for it. After that, a security bug in 14 gets no fix from upstream. So we built that server, loaded 3.1 GB and the pgvector extension onto it, and upgraded it to 18 five different ways.
Upgrades like this are part of the managed database service CloudSpinx runs for organizations in Kenya, on their own servers and in their cloud accounts. The run refused twice before it started, Ubuntu's upgrade command took 433 seconds where one flag made it 12, the two fastest methods made a backup the only way back once 18 was running, and every pg_upgrade mode we ran dropped part of what the planner knows. The commands and output are below.
What PostgreSQL 14 end of life changes on 12 November
PostgreSQL supports each major version for five years, then ships a final minor release and calls it unsupported. Nothing stops working on the day. What stops is the patch. Ubuntu's jammy-updates carries 14.24, the current minor, and the 12 November release is the last one upstream makes, so the next CVE after it lands on a version the PostgreSQL project no longer fixes. If you also run Kubernetes, its clock is far shorter, about 14 months per minor.
Amazon RDS gives 14 a few more months. Its release calendar keeps 14 on standard support to 28 February 2027, and Extended Support pricing starts on 1 March 2027. A server you run yourself gets no further fixes from the PostgreSQL project after 12 November, only whatever your distribution decides to backport, and on our server, adding PGDG for 18 replaced Ubuntu's 14 build with PGDG's, which follows upstream.
Go to 18. It is supported to 14 November 2030, its current minor is 18.6, and it is the first version whose pg_upgrade carries optimizer statistics across, which matters more than it sounds (see the statistics section below). PostgreSQL 19 was at its first release candidate on 15 October, and a major that new is not what you put under an end-of-life deadline.
The two refusals in the upgrade from PostgreSQL 14 to 18
We added the PGDG repository, installed postgresql-18, created an 18 cluster with initdb's defaults and ran pg_upgrade --check against the stopped 14 cluster. It failed in a tenth of a second.
"old cluster does not use data checksums but the new one does"
Performing Consistency Checks
-----------------------------
Checking cluster versions ok
old cluster does not use data checksums but the new one does
Failure, exiting
PostgreSQL 18's initdb turns data checksums on by default, and the 18 release notes say pg_upgrade requires both clusters to match. The cluster Ubuntu created for 14 has them off. You have two fixes. Create the new cluster with initdb --no-data-checksums, which costs nothing and leaves you without checksums. Or stop 14 and run pg_checksums --enable on it first, which rewrote every file of our 3.1 GB database (403,803 blocks) in 61.7 seconds, offline. That time grows with your data.
"Your installation references loadable libraries that are missing from the new installation"
Checking for presence of required libraries fatal
Your installation references loadable libraries that are missing from the
new installation.
...
could not load library "$libdir/vector": ERROR: could not access file "$libdir/vector": No such file or directory
In database: shop
Every extension that loads a library needs that library built for 18 and installed before pg_upgrade runs, and pgvector 0.8.7 was installed for 14 only. After apt install postgresql-18-pgvector the check passed in 0.7 seconds. PostGIS and anything you compiled yourself go through the same library check, so list select extname, extversion from pg_extension in every database before the night, not during it.
pg_upgradecluster defaults to a dump
On Debian and Ubuntu the wrapper is pg_upgradecluster, from postgresql-common, which moves to version 293 when the PGDG packages for 18 go in. Its man page says it plainly: "The default is dump." We ran pg_upgradecluster -v 18 14 main on the kind of Ubuntu server we run and it took 432.9 seconds, including the analyze it runs at the end. For the dump itself it restarts the old cluster "with restricted connections", so plan the whole dump as the outage. pg_upgradecluster -m link -v 18 14 main did the same upgrade in 11.6 seconds, analyze included.
The wrapper earns its keep in two ways. In link mode it passed --no-data-checksums to initdb for us, so the first refusal never happened, and both runs finished by analyzing the new cluster. The dump has a merit too: it rebuilt everything and came out with checksums on. For a few gigabytes and a Sunday that trade is fine. A dump rewrites every row and rebuilds every index, so its time follows the size of what you have.
Copy, link or swap: the speed comes out of your rollback
Raw pg_upgrade gives you the choice directly. We timed copy, link and swap, each several times against an identical copy of the 14 cluster, and marked where pg_upgrade's own output and documentation say each one stops leaving the old cluster usable. The link-mode refusal is the one we tested.
- old 14 still starts
- starts after renaming pg_control.old
- restore from backup
Copy is the only one of the three that copies the data files, so its time follows the bytes on disk. Link and swap buy their speed by handing the same files to 18.
The link-mode warning is in pg_upgrade's own output, "the old cluster cannot be safely started once the new cluster has been started". We started 18, then tried 14. pg_ctlcluster 14 main start failed, and 14's log said why:
but could not open file "/var/lib/postgresql/14/main/global/pg_control": No such file or directory
That refusal is a guard, not the damage. pg_upgrade renames that file to pg_control.old when linking starts, before 18 ever runs. The pg_upgrade documentation says renaming it back is safe only if the new cluster never ran, and otherwise the old one "will need to be restored from backup". So link mode is the right choice when you hold a backup you have actually restored, which is a recovery test rather than a cron job, and the wrong one when nobody has opened the backup since it was set up.
If deciding which mode your data can afford is the part you would rather hand over, send us the pg_lsclusters output and the extension list and we will tell you.
PostgreSQL 18 keeps column statistics, not the ones you created
After a pg_upgrade to 18 in each of copy, link and swap mode, the column statistics from 14 were still there (pg_stats returned 18 rows, as it did on 14), which is the change 18 made to pg_upgrade. The one statistics object we had created with CREATE STATISTICS came back empty, and the documentation says so: statistics "created explicitly with CREATE STATISTICS" are not transferred. Our orders table holds county in lockstep with branch, and without that object the planner multiplies two independent guesses.
CREATE STATISTICS, so the estimate fell from 3,980 rows to 85 until a half-second vacuumdb rebuilt them.85 is the planner multiplying two separate guesses: 3,980 rows for the branch times about 1 in 47 for the county, because nothing left after the upgrade tells it that county follows branch.
On 17 it is worse: pg_upgrade --link to 17 brought no statistics at all, and the same query was planned for 54 rows. Either way the fix is the vacuumdb line pg_upgrade prints at the end. On 18 that is vacuumdb --all --analyze-in-stages --missing-stats-only, which took half a second here and put the estimate back to 3,989. Skip it and any query that leaned on an extended statistic is planned as if those columns were independent until something analyzes that table again. Ours expected 85 rows and got 4,000.

The order we run it in
- Inventory every cluster with
pg_lsclustersand every extension in every database, and confirm a package exists for 18 for each one. - Install
postgresql-18and the extension packages from PGDG, and decide checksums now: off with--no-data-checksums, or on withpg_checksumsin a window long enough to rewrite the data. - Restore last night's backup onto a spare server and run the real upgrade there, timed, the same rehearse-first order we set out for a move off MongoDB Atlas. It is the rehearsal and the backup test in one.
- On the night, take the backup, run
pg_upgrade --check --link, then the upgrade with--link, orpg_upgradecluster -m linkon Ubuntu. - Start 18, run both
vacuumdblines from the summary, point the application at it and watch the slowest queries for an hour. - Take a fresh backup of 18 and restore it once, then run
delete_old_cluster.sh.
One database of a few gigabytes, no replica and a quiet weekend? Run those six steps yourself; this post is enough. The upgrades that hurt are the ones with streaming replicas, extensions nobody remembers installing, or an application that cannot be down for the length of a copy, and we handle those with you from the rehearsal to the day you delete the old cluster.
Send pg_lsclusters from each server, the extension list from each database, the data size, whether a replica follows the primary and how long the application can be down, through the database scoping form or on WhatsApp at +254 713 403 044. We send back the method, the order, the window it needs on your data and one figure for the upgrade. It is free and commits you to nothing.