A PostgreSQL major upgrade changes the database cluster that applications use, so the cutover has to be treated as a controlled migration instead of a routine package restart. The risky moment is the handoff from the old major version to the new one, where rollback depends on backups, the untouched old cluster, and proof that the upgraded server still serves the expected data.
On Debian and Ubuntu systems, postgresql-common manages versioned clusters with tools such as pg_lsclusters, pg_ctlcluster, pg_dropcluster, and pg_upgradecluster. The pg_upgradecluster –method=upgrade workflow uses the newer pg_upgrade binary under the wrapper, creates the target cluster, moves the original port to the new cluster, and keeps the old cluster stopped on another port until it is deliberately removed.
The example path upgrades a package-managed PostgreSQL 17 cluster named main to PostgreSQL 18 on Ubuntu 26.04 with the PostgreSQL Apt Repository available. Use the source and target versions that match the server being upgraded, install target-version extension packages before the cutover, and rehearse the same steps on a restored copy before production traffic is stopped.
$ sudo pg_lsclusters Ver Cluster Port Status Owner Data directory Log file 17 main 5432 online postgres /var/lib/postgresql/17/main /var/log/postgresql/postgresql-17-main.log
$ df -h /var/lib/postgresql Filesystem Size Used Avail Use% Mounted on overlay 118G 39G 74G 35% /
pg_upgradecluster –method=upgrade uses copy mode by default when it calls pg_upgrade, so the upgrade needs enough space for the new cluster data files unless a separately tested link, clone, or filesystem-specific mode is chosen.
$ sudo install --directory --owner=postgres --group=postgres --mode=0700 /var/backups/postgresql
$ sudo -u postgres pg_dumpall --file=/var/backups/postgresql/pg_dumpall-2026-06-07.sql
pg_dumpall output contains database data, role definitions, and possible password hashes. Store it with the same care as production credentials.
$ sudo chmod 0600 /var/backups/postgresql/pg_dumpall-2026-06-07.sql
$ sudo ls -l /var/backups/postgresql/pg_dumpall-2026-06-07.sql -rw------- 1 postgres postgres 4296 Jun 7 05:25 /var/backups/postgresql/pg_dumpall-2026-06-07.sql
A file listing does not prove recoverability. Restore the backup into a separate test target before relying on it for rollback. Related: How to restore a PostgreSQL database backup
$ sudo -u postgres psql -d appdb -c "SELECT * FROM upgrade_check;" id | note ----+---------------------- 1 | before major upgrade (1 row)
Use a real read-only query, migration marker, login check, or health endpoint that proves the application data expected after the upgrade.
$ sudo -u postgres psql -At -c "SELECT extname FROM pg_extension ORDER BY 1;" plpgsql
Clusters that use extensions such as PostGIS need matching target-version packages before the upgrade, for example postgresql-18-postgis-3 when upgrading to PostgreSQL 18.
$ sudo apt-get install postgresql-18 postgresql-client-18 Reading package lists... Done Building dependency tree... Done Reading state information... Done The following NEW packages will be installed: postgresql-18 postgresql-client-18 ##### snipped ##### Setting up postgresql-client-18 (18.4-1.pgdg26.04+1) ... Setting up postgresql-18 (18.4-1.pgdg26.04+1) ...
Use the package source already approved for the server. On Ubuntu 26.04, the distribution packages provide PostgreSQL 18; older source versions for a 17-to-18 rehearsal came from the PostgreSQL Apt Repository.
$ sudo pg_dropcluster --stop 18 main
Drop only a newly-created empty target cluster that was never used. Do not drop a target-version cluster that contains real data or already serves an application.
No clients should write to the old cluster while the upgrade is running. Pause application traffic, schedulers, ingestion workers, and manual sessions according to the maintenance plan.
$ sudo pg_ctlcluster 17 main stop
$ sudo pg_upgradecluster --method=upgrade 17 main
Upgrading cluster 17/main to 18/main ...
Restarting old cluster with restricted connections...
Stopping old cluster...
Creating new PostgreSQL cluster 18/main ...
##### snipped #####
Performing Consistency Checks
-----------------------------
Checking cluster versions ok
Checking database connection settings ok
Checking for presence of required libraries ok
##### snipped #####
Upgrade Complete
----------------
Some statistics are not transferred by pg_upgrade.
##### snipped #####
Success. Please check that the upgraded cluster works. If it does,
you can remove the old cluster with
pg_dropcluster 17 main
Do not remove the old cluster from the completion message until application validation, monitoring, backup, and rollback decisions are finished.
$ sudo pg_lsclusters Ver Cluster Port Status Owner Data directory Log file 17 main 5433 down postgres /var/lib/postgresql/17/main /var/log/postgresql/postgresql-17-main.log 18 main 5432 online postgres /var/lib/postgresql/18/main /var/log/postgresql/postgresql-18-main.log
$ sudo -u postgres pg_isready /var/run/postgresql:5432 - accepting connections
$ sudo -u postgres psql -d appdb -c "SELECT * FROM upgrade_check;" id | note ----+---------------------- 1 | before major upgrade (1 row)
$ sudo -u postgres psql -d appdb -c "SELECT current_setting('server_version') AS server_version;"
server_version
----------------------------------
18.4 (Ubuntu 18.4-1.pgdg26.04+1)
(1 row)
$ sudo -u postgres vacuumdb --all --analyze-in-stages --verbose vacuumdb: processing database "appdb": Generating minimal optimizer statistics (1 target) INFO: analyzing "public.upgrade_check" INFO: "upgrade_check": scanned 1 of 1 pages, containing 1 live rows and 0 dead rows; 1 rows in sample, 1 estimated total rows ##### snipped #####
pg_upgrade transfers most optimizer statistics in recent releases, but some statistics are still missing after the upgrade. Rebuilding statistics gives the planner current information before normal traffic returns.
$ sudo pg_ctlcluster 18 main status pg_ctl: server is running (PID: 7959) /usr/lib/postgresql/18/bin/postgres "-D" "/var/lib/postgresql/18/main" "-c" "config_file=/etc/postgresql/18/main/postgresql.conf"
Rollback to the old cluster is simplest while copy mode has preserved the old cluster and the new cluster has not replaced the only valid recovery point. Link, clone, copy-file-range, and swap modes have different rollback boundaries and must be rehearsed before production use.
$ sudo pg_dropcluster --stop 17 main
pg_dropcluster permanently deletes the old cluster data directory. Keep verified backups and post-upgrade monitoring evidence before running it.
$ sudo apt-get purge postgresql-17 postgresql-client-17
$ sudo pg_lsclusters Ver Cluster Port Status Owner Data directory Log file 18 main 5432 online postgres /var/lib/postgresql/18/main /var/log/postgresql/postgresql-18-main.log