Upgrading PostgreSQL keeps security fixes and performance improvements flowing while preventing the quiet risk of running an out-of-support database. A safe upgrade approach reduces downtime surprises, preserves data integrity, and keeps a rollback option available until applications are confirmed healthy.
Minor updates within the same PostgreSQL major version are typically in-place package upgrades that keep the on-disk format compatible. Major version upgrades change internal catalogs and sometimes on-disk structures, so the server binaries must be upgraded with tooling such as pg_upgrade, which migrates a cluster to the new major version without rebuilding every database from scratch.
Package-based installations on Debian or Ubuntu can keep old and new major versions installed side-by-side as separate clusters under /var/lib/postgresql, managed by postgresql-common tools such as pg_lsclusters and pg_upgradecluster. Major upgrades require a maintenance window, a verified backup stored outside the data directory, enough free disk space for copy-based upgrades, and matching extension packages for the target major version.
Related: How to create a PostgreSQL database backup \\
Related: How to restore a PostgreSQL database backup
Steps to perform a PostgreSQL major version upgrade using pg_upgradecluster:
- Schedule a maintenance window for database downtime.
- List existing PostgreSQL clusters and ports.
$ sudo pg_lsclusters Ver Cluster Port Status Owner Data directory Log file 15 main 5432 online postgres /var/lib/postgresql/15/main /var/log/postgresql/postgresql-15-main.log
- Note the source cluster Ver plus Cluster values for the upgrade (example: 15 / main).
- Check free disk space for the filesystem hosting /var/lib/postgresql.
$ df -h /var/lib/postgresql Filesystem Size Used Avail Use% Mounted on /dev/sda2 200G 85G 106G 45% /
pg_upgradecluster copy mode requires free space close to the size of the source cluster, because a new data directory is created for the target major version.
- Create a backup directory owned by postgres.
$ sudo install --directory --owner=postgres --group=postgres --mode=0700 /var/backups/postgresql
No output indicates the directory was created successfully.
- Create a full logical backup of all databases and global objects.
$ sudo -u postgres pg_dumpall --file=/var/backups/postgresql/pg_dumpall-2025-12-15.sql
pg_dumpall output contains database contents plus role definitions and can include password hashes, so the backup file must be protected like a private key.
- Restrict the backup file permissions.
$ sudo chmod 0600 /var/backups/postgresql/pg_dumpall-2025-12-15.sql
- Verify that the backup file exists and is readable only by its owner.
$ sudo ls -l /var/backups/postgresql/pg_dumpall-2025-12-15.sql -rw------- 1 postgres postgres 1362458123 Dec 15 12:34 /var/backups/postgresql/pg_dumpall-2025-12-15.sql
- Sanity-check the backup header to confirm a valid dump file.
$ sudo -u postgres head -n 6 /var/backups/postgresql/pg_dumpall-2025-12-15.sql -- -- PostgreSQL database dump -- \connect postgres
- Update package indexes.
$ sudo apt update Hit:1 http://archive.ubuntu.com/ubuntu jammy InRelease Get:2 http://security.ubuntu.com/ubuntu jammy-security InRelease [110 kB] ##### snipped ##### Reading package lists... Done Building dependency tree... Done Reading state information... Done
- Install the target PostgreSQL major version packages (example: 16).
$ sudo apt install --assume-yes postgresql-16 postgresql-client-16 Reading package lists... Done Building dependency tree... Done Reading state information... Done The following NEW packages will be installed: postgresql-16 postgresql-client-16 ##### snipped ##### Setting up postgresql-16 ... Creating new PostgreSQL cluster 16/main ... ##### snipped #####
- Confirm that a target-version cluster exists.
$ sudo pg_lsclusters Ver Cluster Port Status Owner Data directory Log file 15 main 5432 online postgres /var/lib/postgresql/15/main /var/log/postgresql/postgresql-15-main.log 16 main 5433 online postgres /var/lib/postgresql/16/main /var/log/postgresql/postgresql-16-main.log
The target cluster commonly starts on port 5433 when the source cluster is already using 5432.
- Stop the target-version cluster created during package installation.
$ sudo pg_ctlcluster 16 main stop
- Stop the source cluster.
$ sudo pg_ctlcluster 15 main stop
- Run the major version upgrade in copy mode.
$ sudo pg_upgradecluster --method=upgrade 15 main Stopping old cluster... Creating new cluster 16/main ... ##### snipped ##### Running /usr/lib/postgresql/16/bin/pg_upgrade ... ##### snipped ##### Success. Please check the new cluster log: /var/log/postgresql/postgresql-16-main.log
Ensure extension packages for the target major version are installed before upgrading (example: postgresql-16-<extension>), because pg_upgrade requires matching extension binaries.
- List clusters to confirm the target version is online on the original port.
$ sudo pg_lsclusters Ver Cluster Port Status Owner Data directory Log file 15 main 5433 down postgres /var/lib/postgresql/15/main /var/log/postgresql/postgresql-15-main.log 16 main 5432 online postgres /var/lib/postgresql/16/main /var/log/postgresql/postgresql-16-main.log
The source cluster is commonly kept in down state on a different port for rollback until it is removed.
- Check that PostgreSQL accepts connections.
$ sudo -u postgres pg_isready /var/run/postgresql:5432 - accepting connections
- Verify the upgraded server version.
$ sudo -u postgres psql -c "SELECT version();" version ----------------------------------------------------------------------------------------------------------------------------- PostgreSQL 16.1 (Ubuntu 16.1-1.pgdg22.04+1) on x86_64-pc-linux-gnu, compiled by gcc, 64-bit (1 row) - Rebuild planner statistics for all databases.
$ sudo -u postgres vacuumdb --all --analyze-in-stages --verbose vacuumdb: processing database "postgres" vacuumdb: processing database "template1" ##### snipped #####
Refreshing statistics helps the query planner make good choices immediately after a major upgrade.
- Check the upgraded cluster service status for active (running) state.
$ sudo systemctl status postgresql@16-main ● postgresql@16-main.service - PostgreSQL Cluster 16-main Loaded: loaded (/lib/systemd/system/postgresql@.service; enabled; vendor preset: enabled) Active: active (running) since Mon 2025-12-15 12:41:10 UTC; 2min ago ##### snipped ##### - Remove the old cluster when rollback is no longer needed.
$ sudo pg_dropcluster --stop 15 main
pg_dropcluster permanently deletes the cluster data directory, so application verification must be complete before removal.
- Purge old major version packages when no longer needed.
$ sudo apt purge --assume-yes postgresql-15 postgresql-client-15 Reading package lists... Done Building dependency tree... Done Reading state information... Done The following packages will be REMOVED: postgresql-15* postgresql-client-15* ##### snipped #####
- Remove orphaned dependencies.
$ sudo apt autoremove --assume-yes Reading package lists... Done Building dependency tree... Done Reading state information... Done The following packages will be REMOVED: ##### snipped #####
- Confirm that only the target cluster remains.
$ sudo pg_lsclusters Ver Cluster Port Status Owner Data directory Log file 16 main 5432 online postgres /var/lib/postgresql/16/main /var/log/postgresql/postgresql-16-main.log
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.
Comment anonymously. Login not required.
