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.

Steps to perform a PostgreSQL major version upgrade using pg_upgradecluster:

  1. Schedule a maintenance window for database downtime.
  2. 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

  3. Note the source cluster Ver plus Cluster values for the upgrade (example: 15 / main).
  4. 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.

  5. 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.

  6. 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.

  7. Restrict the backup file permissions.
    $ sudo chmod 0600 /var/backups/postgresql/pg_dumpall-2025-12-15.sql
  8. 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
  9. 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
    
  10. 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
  11. 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 #####
  12. 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.

  13. Stop the target-version cluster created during package installation.
    $ sudo pg_ctlcluster 16 main stop
  14. Stop the source cluster.
    $ sudo pg_ctlcluster 15 main stop
  15. 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.

  16. 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.

  17. Check that PostgreSQL accepts connections.
    $ sudo -u postgres pg_isready
    /var/run/postgresql:5432 - accepting connections
  18. 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)
  19. 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.

  20. 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 #####
  21. 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.

  22. 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 #####
  23. 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 #####
  24. 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
Discuss the article:

Comment anonymously. Login not required.