PostgreSQL 18 Docker Upgrade: Dump and Restore Method

PostgreSQL 18 introduces a major change to the official Docker image layout. The internal data directory is now version‑specific, which means you cannot simply switch your image tag from postgres:17 to postgres:18 and expect your existing volume to work. This guide walks you through a clean and reliable upgrade using the dump and restore method.

What Changed in PostgreSQL 18?

Starting with PostgreSQL 18, the official Docker image uses a new PGDATA structure: (more info).

PostgreSQL VersionDefault PGDATA PathDocker VOLUME
17 and earlier/var/lib/postgresql/data/var/lib/postgresql/data
18 and later/var/lib/postgresql/18/docker/var/lib/postgresql

This change will make it easier for future major upgrades. By mounting the parent directory, future images can simply create a new sibling folder (e.g., /var/lib/postgresql/19/docker). This allows the pg_upgrade utility to use the --link flag, which performs a nearly instantaneous migration by hard-linking files between versioned directories rather than copying data across volumes.

However, this change means we cannot use the data in the old /var/lib/postgresql/data path and need to migrate it to the new /var/lib/postgresql path.

To successfully move from PostgreSQL 17 to 18, we need to do a standard “dump and restore” or use a specialized upgrade container while simultaneously updating your docker-compose.yml to reflect these new path requirements.

The simultaneously upgrading Docker and running multiple versions is more involved process. The easiest is the “dump and restore” approach and this article will concentrate on it.

Step by step guide to upgrade from Postgres 17 to 18 using dump and restore method

Let’s start with the sample existing docker-compose.yml for our PostgreSQL 17 instance.

services:
  db:
    image: postgres:17
    restart: unless-stopped
    environment:
      - POSTGRES_USER=example_user
      - POSTGRES_PASSWORD=example_password
      - POSTGRES_DB=example_db
    ports:
      - "5432:5432"
    volumes:
      - ./db:/var/lib/postgresql/dataCode language: YAML (yaml)

Here is the error message that you might see if you just change the image: postgres:17 to image: postgres:18 in the above docker-compose file and restart.

Error: in 18+, these Docker images are configured to store database data in a format which is compatible with "pg_ctlcluster" (specifically, using major-version-specific directory names).  This better reflects how PostgreSQL itself works, and how upgrades are to be performed.

See also https://github.com/docker-library/postgres/pull/1259⁠

Counter to that, there appears to be PostgreSQL data in: /var/lib/postgresql/data (unused mount/volume) This is usually the result of upgrading the Docker image without upgrading the underlying database using "pg_upgrade" (which requires both versions).

The suggested container configuration for 18+ is to place a single mount at /var/lib/postgresql which will then place PostgreSQL data in a subdirectory, allowing usage of "pg_upgrade --link" without mount point boundary issues.

See https://github.com/docker-library/postgres/issues/37⁠ for a (long) discussion around this process, and suggestions for how to do so.

Step 1: Review the postgres compatibility

Read the PostgreSQL 18 release notes for any breaking changes and confirm that all extensions, application drivers and tools you use are compatible with PostgreSQL 18.

Always make sure to test the changes in a staging environment before performing it on a production system and always having a solid backup and rollback plan.

Step 2: Export the current data

We will export the data from the old version and then restore in the new version.

You can use the following command to export. Make sure to change the container name (pg17_container) to your container name

docker exec -t pg17_container pg_dumpall -U example_user > backup.sqlCode language: Bash (bash)

Once the backup is created verify the backup.sql file is not empty and contains the schema and data.

Step 3: Stop the old container

With the backup secured, you can safely shut down the old container. It’s also best practice to rename the old data volume directory. This prevents the new PostgreSQL 18 instance from accidentally trying to use the old, incompatible data files.

docker compose down
mv ./db ./db-oldCode language: Bash (bash)

Step 4: Update docker-compose.yml for PostgreSQL 18

Now, modify your docker-compose.yml file. You need to update the image tag to postgres:18 and, most importantly, update the volume path to reflect the new version-specific directory structure used by the official image.

services:
  db:
    image: postgres:18 # Change version number
    restart: unless-stopped
    environment:
      - POSTGRES_USER=example_user
      - POSTGRES_PASSWORD=example_password
      - POSTGRES_DB=example_db
    ports:
      - "5432:5432"
    volumes:
      # IMPORTANT: The PGDATA path is now version-specific
      - ./db:/var/lib/postgresqlCode language: YAML (yaml)

Do not set PGDATA=/var/lib/postgresql/data in PostgreSQL 18. The new image manages versioned directories automatically.

Step 5: Start the New Container

It’s time to bring up the new PostgreSQL 18 container. Docker Compose will pull the postgres:18 image and create a new, empty ./db directory on your host, which will be used to initialize the new database instance.

docker compose up -dCode language: Bash (bash)

Step 6: Restore Your Data

Once the new container is up and running, you can restore your data from the backup file created in Step 1. We will pipe the backup.sql file into the psql command inside the new container (pg18_container).

docker exec -i pg18_container psql -U example_user -d example_db < backup.sqlCode language: Bash (bash)

This command may take some time, depending on the size of your database.

Step 7: Check the Version

As a final confirmation, you can execute a command inside the container to check the running PostgreSQL version.

docker exec -it pg18_container psql -U example_user -d example_dbCode language: Bash (bash)

After connecting, run the following SQL query:

SELECT version();Code language: SQL (Structured Query Language) (sql)

You should see an output confirming you are now running PostgreSQL 18.

Step 8: Clean Up (Optional)

After the restore process finishes, connect to your database with your application or a database client to ensure all data has been restored correctly. Once you have fully verified the integrity of the migration, you can safely remove the old data directory and the SQL backup file to reclaim disk space.

rm -rf ./db-old
rm backup.sqlCode language: Bash (bash)

Limitations of dump and restore method

While this method is simple and reliable, it has drawbacks:

  • Downtime is required
  • Slow for large databases (100GB+)
  • Indexes must be rebuilt, increasing restore time
  • High CPU usage during restore

For large production databases, consider using pg_upgrade instead. PostgreSQL 18 improves pg_upgrade with:

  • Faster parallel checks (--jobs)
  • Hard‑linking support
  • Faster directory switching (--swap)

Leave a Reply