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 upgrade PostgreSQL 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
    overlay         1.8T   28G  1.7T   2% /

    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-2026-01-01.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-2026-01-01.sql
  8. Verify that the backup file exists and is readable only by its owner.
    $ sudo ls -l /var/backups/postgresql/pg_dumpall-2026-01-01.sql
    -rw------- 1 postgres postgres 2058 Jan  1 08:49 /var/backups/postgresql/pg_dumpall-2026-01-01.sql
  9. Sanity-check the backup header to confirm a valid dump file.
    $ sudo -u postgres head -n 6 /var/backups/postgresql/pg_dumpall-2026-01-01.sql
    --
    -- PostgreSQL database cluster dump
    --
    \restrict q4CV77rvL5DUvFpDVqcla1jpBJUv6S6QvS0OeVwmmPSv0kLUeehTxq3yucg92NL
  10. Update package indexes.
    $ sudo apt update
    WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
    
    Hit:1 http://apt.postgresql.org/pub/repos/apt noble-pgdg InRelease
    Hit:2 http://ports.ubuntu.com/ubuntu-ports noble InRelease
    ##### snipped #####
    Reading package lists...
    Building dependency tree...
    Reading state information...
    All packages are up to date.
  11. Install the target PostgreSQL major version packages (example: 16).
    $ sudo apt install --assume-yes postgresql-16 postgresql-client-16
    WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
    
    Reading package lists...
    Building dependency tree...
    Reading state information...
    Suggested packages:
      postgresql-doc-16
    The following NEW packages will be installed:
      postgresql-16 postgresql-client-16
    ##### snipped #####
    Setting up postgresql-16 (16.11-1.pgdg24.04+1) ...
    ##### snipped #####
  12. Confirm the source cluster is online before the upgrade.
    $ 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

    pg_upgradecluster creates the target cluster during the upgrade, so it is normal to see only the source cluster here.

  13. Stop the source cluster.
    $ sudo pg_ctlcluster 15 main stop
  14. Run the major version upgrade in copy mode.
    $ sudo pg_upgradecluster --method=upgrade 15 main
    Upgrading cluster 15/main to 16/main ...
    Restarting old cluster with restricted connections...
    Stopping old cluster...
    Creating new PostgreSQL cluster 16/main ...
    ##### snipped #####
    Success. Please check that the upgraded cluster works. If it does,
    you can remove the old cluster with
        pg_dropcluster 15 main

    Ensure extension packages for the target major version are installed before upgrading (example: postgresql-16-<extension>), because pg_upgrade requires matching extension binaries.

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

  16. Check that PostgreSQL accepts connections.
    $ sudo -u postgres pg_isready
    /var/run/postgresql:5432 - accepting connections
  17. Verify the upgraded server version.
    $ sudo -u postgres psql -c "SELECT version();"
                                                                      version                                                                  
    -------------------------------------------------------------------------------------------------------------------------------------------
     PostgreSQL 16.11 (Ubuntu 16.11-1.pgdg24.04+1) on aarch64-unknown-linux-gnu, compiled by gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit
    (1 row)
  18. Rebuild planner statistics for all databases.
    $ sudo -u postgres vacuumdb --all --analyze-in-stages --verbose
    vacuumdb: processing database "postgres": Generating minimal optimizer statistics (1 target)
    ##### snipped #####
    vacuumdb: processing database "template1": Generating minimal optimizer statistics (1 target)
    ##### snipped #####

    Refreshing statistics helps the query planner make good choices immediately after a major upgrade.

  19. 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 (/usr/lib/systemd/system/postgresql@.service; enabled-runtime; preset: enabled)
         Active: active (running) since Thu 2026-01-01 08:49:17 UTC; 941ms ago
    ##### snipped #####
  20. 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.

  21. Purge old major version packages when no longer needed.
    $ sudo apt purge --assume-yes postgresql-15 postgresql-client-15
    WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
    
    Reading package lists...
    Building dependency tree...
    Reading state information...
    The following packages will be REMOVED:
      postgresql-15* postgresql-client-15*
    0 upgraded, 0 newly installed, 2 to remove and 0 not upgraded.
    After this operation, 64.4 MB disk space will be freed.
    ##### snipped #####
  22. Remove orphaned dependencies.
    $ sudo apt autoremove --assume-yes
    WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
    
    Reading package lists...
    Building dependency tree...
    Reading state information...
    0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
  23. 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