{"id":2935,"date":"2026-11-02T10:40:36","date_gmt":"2026-11-02T10:40:36","guid":{"rendered":"https:\/\/hostry.com\/blog\/?p=2935"},"modified":"2026-11-02T10:40:38","modified_gmt":"2026-11-02T10:40:38","slug":"postgresql-14-end-of-life-upgrade","status":"publish","type":"post","link":"https:\/\/hostry.com\/blog\/postgresql-14-end-of-life-upgrade\/","title":{"rendered":"PostgreSQL 14 End of Life on November 12: Upgrade to 18 on a VPS"},"content":{"rendered":"\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"675\" src=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-cover.webp\" alt=\"PostgreSQL 14 end of life: upgrading a database cluster to PostgreSQL 18 on a Linux VPS\" class=\"wp-image-2930\" srcset=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-cover.webp 1200w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-cover-300x169.webp 300w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-cover-1024x576.webp 1024w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-cover-450x253.webp 450w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-cover-768x432.webp 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><\/figure>\n\n\n\n<pre class=\"wp-block-preformatted\" style=\"font-size:14px\"><em><strong>As of November 2, 2026, PostgreSQL 14 has 10 days of upstream support left, with its final minor release due on November 12. After that release, the PostgreSQL project publishes no more fixes for version 14, security fixes included, so the database should be on PostgreSQL 18 before the next round of advisories lands.<\/strong><\/em><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Nothing shuts down on November 13. <strong>PostgreSQL 14 end of life<\/strong> means the installed binaries keep serving queries, and every bug or vulnerability found afterwards stays unpatched on your machine. On an unmanaged <strong><a href=\"https:\/\/hostry.com\/products\/vps\/\">Hostry KVM VPS<\/a><\/strong> you run the major upgrade yourself, and on Ubuntu or Debian the tools for it ship with the PostgreSQL packages. The steps assume one server with one 14 cluster.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">PostgreSQL 14 end of life: what stops on November 12<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The <strong><a href=\"https:\/\/www.postgresql.org\/support\/versioning\/\" target=\"_blank\" rel=\"noopener\">PostgreSQL versioning policy<\/a><\/strong> gives each major five years, so the PostgreSQL 14 EOL date is November 12, 2026. A server still on an older 14.x minor than 14.24 also lacks security fixes that have already been published.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Major version<\/th><th>Latest minor (November 2, 2026)<\/th><th>Final release<\/th><\/tr><\/thead><tbody><tr><td>18<\/td><td>18.6<\/td><td>November 14, 2030<\/td><\/tr><tr><td>17<\/td><td>17.11<\/td><td>November 8, 2029<\/td><\/tr><tr><td>16<\/td><td>16.15<\/td><td>November 9, 2028<\/td><\/tr><tr><td>15<\/td><td>15.19<\/td><td>November 11, 2027<\/td><\/tr><tr><td>14<\/td><td>14.24<\/td><td>November 12, 2026<\/td><\/tr><tr><td>13<\/td><td>13.23<\/td><td>Ended November 13, 2025<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Those are the PostgreSQL supported versions as the project lists them. Ubuntu 22.04 also carries 14 in its own archive (build <span class=\"highlighted\">14.24-0ubuntu0.22.04.1<\/span>); unless your distribution has committed to patching it after PostgreSQL 14 end of life, plan as if it will not.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Inventory the PostgreSQL 14 cluster first<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The output of a few commands decides which method is possible. Start with the cluster list, the exact version and whether data checksums are on:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>pg_lsclusters\nsudo -u postgres psql -Atc \"SELECT version();\"\nsudo -u postgres psql -Atc \"SHOW data_checksums;\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A 14 cluster answers <span class=\"highlighted\">off<\/span> to the last query unless someone enabled checksums at <span class=\"highlighted\">initdb<\/span> time, since they were opt-in until version 18. Write that answer down; it matters in the upgrade step.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Next, list the extensions in every database and the PostgreSQL packages installed for 14. Each extension package, such as <span class=\"highlighted\">postgresql-14-postgis-3<\/span>, needs a matching 18 package before an in-place upgrade can work:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>for db in $(sudo -u postgres psql -Atc \"SELECT datname FROM pg_database WHERE datallowconn\"); do\n  echo \"== $db\"\n  sudo -u postgres psql -d \"$db\" -Atc \"SELECT extname || ' ' || extversion FROM pg_extension\"\ndone\ndpkg -l 'postgresql-14*' | grep ^ii<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then size and free space, plus any roles that still log in with MD5 hashes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo -u postgres psql -c \"SELECT datname, pg_size_pretty(pg_database_size(datname)) FROM pg_database ORDER BY pg_database_size(datname) DESC;\"\nsudo du -sh \/var\/lib\/postgresql\/14\/main\ndf -h \/var\/lib\/postgresql\nsudo -u postgres psql -Atc \"SELECT rolname FROM pg_authid WHERE rolpassword LIKE 'md5%';\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Compare the <span class=\"highlighted\">du<\/span> figure with the free space from <span class=\"highlighted\">df<\/span>. If free space is smaller than the data directory, only link mode fits on this disk as it stands. Hostry VPS resources can be scaled up, so check whether the next plan&#8217;s disk covers a second copy; we would rather scale for the window than clear room on a live database server, where hurried deletions cause their own outages.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Upgrade PostgreSQL 14 to 18 or stop at 17?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You can go from 14 straight to 18 without installing 15, 16 or 17 in between: the <strong><a href=\"https:\/\/www.postgresql.org\/docs\/18\/pgupgrade.html\" target=\"_blank\" rel=\"noopener\">pg_upgrade documentation for version 18<\/a><\/strong> supports upgrades from 9.2.X and later, and a dump from 14 restores into 18 as well.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Before you pick the target, do two checks. First, read &#8220;Migration to Version 18&#8221; in the <strong><a href=\"https:\/\/www.postgresql.org\/docs\/18\/release-18.html\" target=\"_blank\" rel=\"noopener\">PostgreSQL 18 release notes<\/a><\/strong> against your application. Two examples from that list: <span class=\"highlighted\">VACUUM<\/span> and <span class=\"highlighted\">ANALYZE<\/span> now also process inheritance children unless you add <span class=\"highlighted\">ONLY<\/span>, and <span class=\"highlighted\">COPY FROM<\/span> no longer treats <span class=\"highlighted\">\\.<\/span> as end-of-file in CSV files. Second, confirm every extension has an 18 package with <span class=\"highlighted\">apt-cache policy postgresql-18-postgis-3<\/span> (with your extension&#8217;s name) once the repository from step 1 below is added.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If both checks pass, go to 18. It is supported until November 2030, a year longer than 17, and 18.6 is its sixth minor release. Two changes in 18 also make the upgrade itself easier: <span class=\"highlighted\">pg_upgrade<\/span> now carries most optimizer statistics over, and its database checks run in parallel under <span class=\"highlighted\">&#8211;jobs<\/span>. Pick 17 if an extension you depend on has no 18 package yet or the application is not ready for an 18 change.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As of November 2, PostgreSQL 19 is at release candidate stage: RC 1 shipped on October 15, and the project plans general availability for November 19, 2026, a week after PostgreSQL 14 end of life. A .0 release is a poor target for a deadline upgrade. Once 19 has had a few minor releases, the step from 18 to 19 can be planned like any other maintenance job.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Back up off the server and test the restore<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every rollback path below ends in &#8220;restore from backup&#8221; at some point, so make the backup before touching packages. Run these as your login user (examples use <span class=\"highlighted\">deploy<\/span> and <span class=\"highlighted\">appdb<\/span>; substitute your own). The shell redirect writes the files as that user, so <span class=\"highlighted\">postgres<\/span> needs no write access to your home directory:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir -p ~\/pg14-backup &amp;&amp; chmod 700 ~\/pg14-backup\nsudo -u postgres pg_dumpall --globals-only &gt; ~\/pg14-backup\/globals.sql\nsudo -u postgres pg_dump -Fc appdb &gt; ~\/pg14-backup\/appdb.dump\nsudo -u postgres psql -d appdb -Atc \"SELECT count(*) FROM orders;\"\nrsync -av ~\/pg14-backup\/ deploy@203.0.113.10:pg14-backup\/<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><span class=\"highlighted\">globals.sql<\/span> holds roles with their password hashes, so keep it as private as the dump. Repeat the <span class=\"highlighted\">pg_dump<\/span> line for each database, and keep the row counts of a few large tables to compare later. Swap <span class=\"highlighted\">203.0.113.10<\/span> for your second machine.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Before you rely on the backup, <strong>restore it on a second VPS<\/strong>. On that test server:<\/p>\n\n\n\n<ol class=\"wp-block-list\"><li>Install PostgreSQL 18 from the PGDG repository, as in step 1 below.<\/li><li>Keep port 5432 off the internet: leave <span class=\"highlighted\">listen_addresses<\/span> at its default <span class=\"highlighted\">localhost<\/span> and allow only SSH in the firewall, as in our <strong><a href=\"https:\/\/hostry.com\/blog\/secure-new-linux-vps-first-30-minutes\/\">UFW and SSH key setup for a fresh Ubuntu server<\/a><\/strong>.<\/li><li>Restore the globals, then the database, and compare the row count:<\/li><\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo -u postgres psql -q &lt; ~\/pg14-backup\/globals.sql\nsudo -u postgres pg_restore --create -d postgres &lt; ~\/pg14-backup\/appdb.dump\nsudo -u postgres psql -d appdb -Atc \"SELECT count(*) FROM orders;\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">An <span class=\"highlighted\">ERROR: role &#8220;postgres&#8221; already exists<\/span> line from the globals file is expected and harmless. Time the restore: it is the real downtime figure for the dump method on your data. Point a staging copy of the application at this server too.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">pg_upgrade &#8211;link vs dump and restore<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Method<\/th><th>Downtime<\/th><th>Extra disk on the server<\/th><th>Rollback<\/th><\/tr><\/thead><tbody><tr><td>Dump and restore (<span class=\"highlighted\">pg_upgradecluster<\/span> default)<\/td><td>The whole run; grows with data size, since every row is reloaded and every index rebuilt<\/td><td>A full second copy of the data in the new cluster<\/td><td>Old cluster untouched; drop the new one and start 14 again<\/td><\/tr><tr><td><span class=\"highlighted\">pg_upgrade<\/span> in copy mode<\/td><td>Time to copy the data files<\/td><td>A second copy of the data files<\/td><td>Old cluster untouched<\/td><\/tr><tr><td><span class=\"highlighted\">pg_upgrade &#8211;link<\/span><\/td><td>Mostly the schema dump and restore; data files are hard-linked<\/td><td>Little; both data directories must be on one filesystem<\/td><td>Only until the new cluster first starts; after that, from backup<\/td><\/tr><tr><td>Logical replication to a second VPS<\/td><td>A switchover of several seconds, per the docs<\/td><td>None on the old server; the copy lives elsewhere<\/td><td>14 keeps running until you switch<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"800\" src=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-upgrade-methods.webp\" alt=\"Three ways to upgrade PostgreSQL 14 to 18: dump and restore, pg_upgrade --link, logical replication, compared by downtime, disk and rollback\" class=\"wp-image-2933\" srcset=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-upgrade-methods.webp 1200w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-upgrade-methods-300x200.webp 300w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-upgrade-methods-1024x683.webp 1024w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-upgrade-methods-450x300.webp 450w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-upgrade-methods-768x512.webp 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><figcaption>The three main paths from PostgreSQL 14 to 18 and what each one costs in downtime, disk and rollback<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">For one VPS, our default is the dump method whenever the timed test restore fits inside your maintenance window. It is slower, the old cluster stays intact, and with postgresql-common it turns data checksums on in the 18 cluster. Use <span class=\"highlighted\">&#8211;link<\/span> when the database is too large for that window or the disk cannot hold a second copy, and accept that the backup is then your rollback.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><span class=\"highlighted\">&#8211;clone<\/span> would give link-mode speed while keeping the old cluster usable, but on Linux the pg_upgrade documentation lists only Btrfs and XFS created with reflink support. On an ext4 root disk, common on VPS images, the run errors out.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Logical replication is the near-zero-downtime option, and the <strong><a href=\"https:\/\/www.postgresql.org\/docs\/18\/upgrading.html\" target=\"_blank\" rel=\"noopener\">upgrading chapter of the PostgreSQL manual<\/a><\/strong> describes it as a standby on the new version that you switch to once it catches up. Schema changes, sequence values and large objects are not replicated, so sequences must be moved forward on the new primary before the application writes to it, either from the publisher&#8217;s values or from the tables, for example <span class=\"highlighted\">SELECT setval(pg_get_serial_sequence(&#8216;orders&#8217;, &#8216;id&#8217;), (SELECT max(id) FROM orders));<\/span> for each serial or identity column.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How to upgrade PostgreSQL 14 to 18 on Ubuntu or Debian<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">On Ubuntu and Debian, upstream <span class=\"highlighted\">pg_upgrade<\/span> comes wrapped in <span class=\"highlighted\">pg_upgradecluster<\/span>, which also copies the configuration from <span class=\"highlighted\">\/etc\/postgresql\/14\/main<\/span>, moves the old cluster to a spare port and gives the new one the original port. Use it unless you built PostgreSQL yourself.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>1. Add the PGDG repository and install PostgreSQL 18<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ubuntu 22.04 and 24.04 do not ship 18, so take it from the project&#8217;s own apt repository, as the <strong><a href=\"https:\/\/www.postgresql.org\/download\/linux\/ubuntu\/\" target=\"_blank\" rel=\"noopener\">PostgreSQL download page for Ubuntu<\/a><\/strong> describes. The script adds the repository under <span class=\"highlighted\">\/etc\/apt\/sources.list.d\/<\/span> (as <span class=\"highlighted\">pgdg.list<\/span> on 22.04, <span class=\"highlighted\">pgdg.sources<\/span> on 24.04) and asks you to press Enter:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt install -y postgresql-common\nsudo \/usr\/share\/postgresql-common\/pgdg\/apt.postgresql.org.sh\nsudo apt install -y postgresql-18\npg_lsclusters<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Install the 18 builds of every extension package from your inventory in the same command. The package creates an empty <span class=\"highlighted\">18\/main<\/span> cluster, which <span class=\"highlighted\">pg_lsclusters<\/span> now shows on the next free port, usually 5433.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>2. Run the checks while 14 is still serving<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><span class=\"highlighted\">pg_upgradecluster &#8211;check<\/span> only confirms that every <span class=\"highlighted\">postgresql-14-*<\/span> package has an 18 counterpart. For the real compatibility checks, run upstream <span class=\"highlighted\">pg_upgrade &#8211;check<\/span> against an 18 cluster whose checksum setting matches the old one. With checksums off on 14, that means recreating <span class=\"highlighted\">18\/main<\/span>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo pg_dropcluster 18 main --stop\nsudo pg_createcluster 18 main -- --no-data-checksums\ncd \/tmp\nsudo -u postgres \/usr\/lib\/postgresql\/18\/bin\/pg_upgrade --check --link \\\n  -b \/usr\/lib\/postgresql\/14\/bin -B \/usr\/lib\/postgresql\/18\/bin \\\n  -d \/etc\/postgresql\/14\/main -D \/etc\/postgresql\/18\/main -p 5432\nsudo pg_dropcluster 18 main\nsudo pg_upgradecluster --check -v 18 14 main<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Leave out <span class=\"highlighted\">&#8212; &#8211;no-data-checksums<\/span> if 14 already had checksums on. <span class=\"highlighted\">-p<\/span> is the port of the running 14 cluster; the new cluster is started on pg_upgrade&#8217;s default 50432. The pg_upgrade run should end with <span class=\"highlighted\">*Clusters are compatible*<\/span>. The second <span class=\"highlighted\">pg_dropcluster<\/span> matters, because <span class=\"highlighted\">pg_upgradecluster<\/span> refuses to run, even with <span class=\"highlighted\">&#8211;check<\/span>, while <span class=\"highlighted\">18\/main<\/span> exists.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>3. Upgrade in the maintenance window<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Decide first whether you want a pause before 18 starts. <span class=\"highlighted\">pg_upgradecluster<\/span> starts the new cluster by default when 14 was running or upgrade hooks are present. With link mode, that first start ends the cheap rollback, so add <span class=\"highlighted\">&#8211;no-start<\/span> if you want to inspect the result before committing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then stop whatever writes to the database, take a fresh dump if data changed since the backup, and run one of these:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl stop myapp\n# dump method (default):\nsudo pg_upgradecluster -v 18 14 main\n# or link mode:\nsudo pg_upgradecluster -v 18 -m upgrade --link 14 main<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Per the <strong><a href=\"https:\/\/manpages.debian.org\/testing\/postgresql-common\/pg_upgradecluster.1.en.html\" target=\"_blank\" rel=\"noopener\">pg_upgradecluster man page<\/a><\/strong>, the old cluster moves to another port with startup set to manual, and the new one takes port 5432 and starts. For the upgrade method, postgresql-common 293 creates the 18 cluster with <span class=\"highlighted\">&#8211;no-data-checksums<\/span> when 14 had none.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"800\" src=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-port-swap.webp\" alt=\"pg_upgradecluster before and after: PostgreSQL 14 cluster on port 5432 becomes an 18 cluster on 5432 while the old 14 cluster moves to a spare port with manual start\" class=\"wp-image-2937\" srcset=\"https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-port-swap.webp 1200w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-port-swap-300x200.webp 300w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-port-swap-1024x683.webp 1024w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-port-swap-450x300.webp 450w, https:\/\/hostry.com\/blog\/wp-content\/uploads\/2026\/11\/postgresql-14-end-of-life-upgrade-port-swap-768x512.webp 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><figcaption>What pg_upgradecluster does to ports and startup. After the dump method the old 14 cluster stays usable as a fallback; after link mode it is not, once 18 has started<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\" style=\"font-size:14px\"><strong>4. Refresh statistics and extensions<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Current postgresql-common runs both <span class=\"highlighted\">vacuumdb<\/span> commands below from its <span class=\"highlighted\">analyze<\/span> hook when it starts the new cluster. Run them by hand only after <span class=\"highlighted\">&#8211;no-start<\/span> or a plain pg_upgrade run. The first fills in statistics pg_upgrade could not carry over, such as extended statistics, and the second updates the counters autovacuum relies on:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo -u postgres \/usr\/lib\/postgresql\/18\/bin\/vacuumdb --all --analyze-in-stages --missing-stats-only\nsudo -u postgres \/usr\/lib\/postgresql\/18\/bin\/vacuumdb --all --analyze-only<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Extensions are a separate step. With the upgrade method, pg_upgrade may leave <span class=\"highlighted\">update_extensions.sql<\/span> in the log directory, with lines such as <span class=\"highlighted\">ALTER EXTENSION &#8220;pgcrypto&#8221; UPDATE;<\/span>. Find it, apply it with <span class=\"highlighted\">sudo -u postgres psql -f<\/span> and the file&#8217;s path, then start the application:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ls \/var\/log\/postgresql\/pg_upgradecluster-14-18-main.*\/<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Verify the upgrade before deleting anything<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>pg_lsclusters\nsudo -u postgres psql -Atc \"SELECT version();\"\nsudo -u postgres psql -d appdb -Atc \"SELECT count(*) FROM orders;\"\nsudo tail -n 50 \/var\/log\/postgresql\/postgresql-18-main.log<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><span class=\"highlighted\">pg_lsclusters<\/span> should show 18 online on 5432 and 14 down on another port. Compare the row counts with the ones you saved, then have the application write a record and read it back.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If the MD5 query from the inventory returned roles, PostgreSQL 18 still accepts them, but MD5 is deprecated for removal in a future major, and setting an MD5 password now raises a warning. Reset those passwords while <span class=\"highlighted\">password_encryption<\/span> is at its default <span class=\"highlighted\">scram-sha-256<\/span>, then switch the matching <span class=\"highlighted\">pg_hba.conf<\/span> lines from <span class=\"highlighted\">md5<\/span> to <span class=\"highlighted\">scram-sha-256<\/span>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keep the old cluster for a few days if you used the dump method; it costs only disk. Then remove it and the 14 packages with <span class=\"highlighted\">sudo pg_dropcluster 14 main<\/span> and <span class=\"highlighted\">sudo apt purge &#8216;postgresql-14*&#8217;<\/span>. After link mode the old cluster is unusable anyway, so drop it once 18 has been checked.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">If pg_upgrade fails<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The messages quoted below were reproduced with PostgreSQL 14.24, PostgreSQL 18.6 and postgresql-common 293.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Data checksums do not match<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">pg_upgrade stops with <span class=\"highlighted\">old cluster does not use data checksums but the new one does<\/span> when the 18 cluster came from a default <span class=\"highlighted\">initdb<\/span>, which turns checksums on since version 18. Recreate it with <span class=\"highlighted\">&#8211;no-data-checksums<\/span> as in step 2. You can enable checksums later with <span class=\"highlighted\">sudo -u postgres \/usr\/lib\/postgresql\/18\/bin\/pg_checksums &#8211;enable -D \/var\/lib\/postgresql\/18\/main<\/span> while the cluster is stopped; it rewrites every block, so budget downtime for it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Missing extension libraries<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><span class=\"highlighted\">Your installation references loadable libraries that are missing from the new installation<\/span> means a <span class=\"highlighted\">.so<\/span> file used by 14 has no 18 build. The message points to <span class=\"highlighted\">loadable_libraries.txt<\/span> under the new data directory, which names the library and the database.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Install the matching <span class=\"highlighted\">postgresql-18-*<\/span> package, or drop the extension on 14 if nothing uses it. Do not run <span class=\"highlighted\">CREATE EXTENSION<\/span> in the new cluster yourself, since pg_upgrade copies the definitions.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Cluster exists, port in use or server still running<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><span class=\"highlighted\">Error: target cluster 18\/main already exists<\/span> comes from <span class=\"highlighted\">pg_upgradecluster<\/span>; run <span class=\"highlighted\">sudo pg_dropcluster 18 main &#8211;stop<\/span> on the empty cluster first.<\/li><li><span class=\"highlighted\">When checking a live server, the old and new port numbers must be different<\/span> appears if <span class=\"highlighted\">-p<\/span> and <span class=\"highlighted\">-P<\/span> are equal during <span class=\"highlighted\">&#8211;check<\/span>.<\/li><li><span class=\"highlighted\">There seems to be a postmaster servicing the old cluster<\/span> means a real run started while 14 was up; stop it with <span class=\"highlighted\">sudo pg_ctlcluster 14 main stop<\/span>.<\/li><\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Locale, encoding and an OS upgrade at the same time<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">An 18 cluster created with a different locale and encoding still passes <span class=\"highlighted\">&#8211;check<\/span> with 18.6, because pg_upgrade sets the new databases&#8217; locale and encoding from the old cluster. The bigger risk is index ordering: the man page warns that a new <span class=\"highlighted\">libc<\/span> or <span class=\"highlighted\">libicu<\/span> can change collation and require index rebuilds after an upgrade method. Do the PostgreSQL upgrade and the Ubuntu release upgrade in separate windows.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Rolling back<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Decide on rollback before the application writes to 18. Until then, going back to 14 loses nothing. After writes resume, the new rows exist only in 18, and a rollback discards them unless you export and reload them by hand, which usually means a second outage. When in doubt, keep the application stopped until the verify checks pass.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After the dump method or copy mode, 14 is intact. Drop the new cluster, give 14 its port back and set it to start on boot:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo pg_dropcluster 18 main --stop\nsudo pg_conftool 14 main set port 5432\necho auto | sudo tee \/etc\/postgresql\/14\/main\/start.conf\nsudo pg_ctlcluster 14 main start<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Link mode leaves a narrow window. If pg_upgrade failed before linking, 14 is unmodified. If linking ran but 18 never started, rename <span class=\"highlighted\">global\/pg_control.old<\/span> back to <span class=\"highlighted\">pg_control<\/span> in the 14 data directory and start it. Once 18 has started, the old files are unsafe and the off-server backup is the way back. With <span class=\"highlighted\">pg_upgradecluster<\/span>, that window stays open only if you passed <span class=\"highlighted\">&#8211;no-start<\/span> in step 3.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Rehearse the upgrade on a spare server first.<\/strong> Size a <strong><a href=\"https:\/\/hostry.com\/products\/vps\/\">Hostry KVM VPS<\/a><\/strong> from the inventory you took at the start, with disk for the data directory plus the dump, then restore the backup, time it and run <span class=\"highlighted\">pg_upgradecluster<\/span> end to end. Plans in the <strong><a href=\"https:\/\/hostry.com\/services\/vps\/\">Hostry VPS catalog<\/a><\/strong> start from $6\/mo at the time of writing, billed monthly.<\/p>\n<!-- AddThis Advanced Settings generic via filter on the_content --><!-- AddThis Share Buttons generic via filter on the_content -->","protected":false},"excerpt":{"rendered":"<p>PostgreSQL 14 gets its final release on November 12, 2026. Check what your server runs, back up off the box, pick dump\/restore or pg_upgrade &#8211;link, a&#8230;<\/p>\n","protected":false},"author":6,"featured_media":2931,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1,102],"tags":[],"class_list":["post-2935","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-virtual-private-server","category-database"],"read_time":10,"_links":{"self":[{"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/posts\/2935","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/comments?post=2935"}],"version-history":[{"count":1,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/posts\/2935\/revisions"}],"predecessor-version":[{"id":2939,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/posts\/2935\/revisions\/2939"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/media\/2931"}],"wp:attachment":[{"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/media?parent=2935"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/categories?post=2935"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hostry.com\/blog\/wp-json\/wp\/v2\/tags?post=2935"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}