Upgrading to PostgreSQL 18 in Docker: A Crucial Change You Need To Be Aware Of

Upgrading to PostgreSQL 18 in Docker: A Crucial Change You Need To Be Aware Of

The PostgreSQL 18 Docker Image has dropped, packed with powerful new features like Asynchronous I/O (AIO) for faster reads and native UUIDv7 support. For those of us running Postgres in Docker, however, the most critical update isn't necessarily a feature, but a fundamental change to the official Docker image that you must know before you upgrade.

In short: you cannot simply change the image tag from postgres:17 to postgres:18 and point it at your old data volume.

This is because the default storage path inside the container has changed. Trying to start a postgres:18 container on a postgres:17 volume will fail.

This guide will walk you through why this change was made and provide a step-by-step process for safely migrating your Dockerized data using a dump and restore.

Why the Big Change? A "One-Time Hassle" for a Long-Term Fix

If you're like me, you are probably wondering why the container maintainers would introduce a change that breaks the normal upgrade path. The answer is to make all future upgrades (from 18 to 19, 19 to 20, etc.) significantly faster and easier.

This change is all about enabling the pg_upgrade utility.

  • The Old Problem (PG 17 and Older): Your volume was mounted directly to the container's data directory: /var/lib/postgresql/data. This made it very difficult to use the pg_upgrade utility, which needs to access both the old and new data directories simultaneously. This left most users with the manual process of a full dump and restore, often involving complex volume management.
  • The New Solution (PG 18 and Newer): The official image now defines its main VOLUME at the parent directory: /var/lib/postgresql. When you launch a postgres:18 container, it creates its data cluster inside a versioned subdirectory, such as /var/lib/postgresql/18/docker.
  • The Future Benefit: When PostgreSQL 19 is released, you will launch a postgres:19 container mounting the same host volume. It will create its new, empty cluster at /var/lib/postgresql/19/docker, right next to the old one. From there, you can docker exec into the new container and run pg_upgrade --link, pointing it to the old and new data paths. This will perform a nearly-instantaneous upgrade.

So, while this first migration from 17 to 18 is still a manual dump-and-restore process with an alteration to your docker command or compose file, you are moving your data into this new, future-proof structure.

The Step-by-Step Docker Migration Guide (<=17 to 18)

This guide assumes you are migrating from a postgres:17 (or older) container to postgres:18. This guide will work for migrations from versions prior to 17 as well.

Before You Begin: BACK UP. This process is destructive to the old container. While your data volume will be preserved (and we'll back it up), you should always have a complete, verified backup of your database before attempting a major upgrade.

Step 1: Dump Your Current Database(s)

From your Docker host, execute pg_dumpall inside your existing container. This command dumps all databases, users (roles), and global objects into a single SQL file on your host.

Replace [container_name] with your current running container's name:

docker exec -t [container_name] pg_dumpall -U postgres > full_cluster_dump.sql

You will now have a full_cluster_dump.sql file in your current directory. (For production systems, be mindful of file permissions on this dump. For more advanced strategies on securing this file or handling very large databases, you can see my relevant article here.)

You won't necessarily want to encrypt this archive because you would just need to decrypt it again within a minute or two. However, you will want to pay attention to the permissions on this archive and make sure that only you can read or modify it.

Step 2: Stop Your Old Container And Rename The Volume

To find out the volume name or the bind mount path:

docker inspect [container_name]

Look for the mounts section. Make note of the bind mount path or volume name.

Now that you have backed up your cluster and you know the location of your volume, the next step is to shut down the version 17 container.

Option A: Using docker stop

docker stop [container_name]
docker rm [container_name]

Option B: Using docker-compose.yml

docker compose down

If you're using a named volume, let's rename the current volume and then recreate its blank replacement:

docker volume rename [docker_volume_name] [docker_volume_name_bak]
docker volume create [docker_volume_name]

Or if you have a bind-mounted volume:

mv [host_path_to_bind_mount] [host_path_to_bind_mount_bak]
mkdir [host_path_to_bind_mount]

Step 3: Launch the New PostgreSQL 18 Container

This is the most critical step. We need to modify your named or bind mount volume to the new container path: /var/lib/postgresql.

Option A: Using docker run

Notice the new image tag (:18) and the new volume target path.

docker run -d --name [container_name] \
  -e POSTGRES_PASSWORD=your_secret_password \
  -v [volume_name_or_host_path]:/var/lib/postgresql \
  -p 5432:5432 \
  postgres:18

Option B: Using docker-compose.yml

This is how you would update your docker-compose.yml file.

services:
  db:
    image: postgres:18  # <-- New image
    # ...
    volumes:
      - [volume_name_or_host_path]:/var/lib/postgresql  # <-- New path

Run docker-compose up -d to start your new, empty PostgreSQL 18 container.

Step 4: Restore Your Data

Now we'll copy the dump file into the new container and restore it.

# 1. Copy the SQL dump into the new container
docker cp full_cluster_dump.sql [container_name]:/tmp/full_cluster_dump.sql

# 2. Execute psql inside the container to restore the data
docker exec -i [container_name] psql -U postgres -f /tmp/full_cluster_dump.sql

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

Step 5: Analyze and Clean Up

The restore process does not build query planner statistics. You must run ANALYZE to prevent your applications from being extremely slow.

# Run this inside the new container
docker exec -i [container_name] vacuumdb --all --analyze-in-stages -U postgres

Once you have thoroughly tested your applications and confirmed all data is present, you can safely delete the dump file and your old, renamed volume backup:

rm full_cluster_dump.sql

And to clean up the old volume:

docker volume rm [docker_volume_name_bak]

Or if you used a bind mount:

rm -rf [host_path_to_bind_mount_bak]

Conclusion

You're all set! Your database is now running on PostgreSQL 18 and, more importantly, it's now using the new volume structure. While this upgrade was a manual process, your next upgrade (e.g., to version 19) will be significantly faster, thanks to the very change that prompted this guide.

Need help or have some feedback? Please post a comment!

Read more