For personal use, I run a lot of service with docker-compose on a single host. In most cases this means having one docker-compose file per service and a reverse proxy on the host for tls termination.

Over time, a number of services started to depend on a quite old version of postgresql - I found two services using postgresql 9.6 for with the last and final version was just released a few days ago.

This means it was time to upgrade the database instances to a later version. I decided to use the latest version, postgresql 14.

In order to not have to lookup how it works, I documented all steps in this blog post.

As an example, I use the miniflux rss reader service with the following docker-compose.yml:

version: "3.6"
services:
  miniflux:
    image: miniflux/miniflux:2.0.33
    ports:
      - "127.0.0.1:<high_port_on_host>:8080"
    depends_on:
      - db
    restart: always
    environment:
      - DATABASE_URL=postgres://miniflux:<db_password>@db/miniflux?sslmode=disable
      - RUN_MIGRATIONS=1
      - CREATE_ADMIN=1
      - BASE_URL=<url>
      - ROOT_URL=<url>
      - ADMIN_USERNAME=<admin_user>
      - ADMIN_PASSWORD=<admin_password>
      - POLLING_FREQUENCY=10
  db:
    image: postgres:9.6
    restart: always
    environment:
      - POSTGRES_USER=<user>
      - POSTGRES_PASSWORD=<password>
    volumes:
      - miniflux-db:/var/lib/postgresql/data

volumes:
  miniflux-db:
    external: true

Upgrading postgresql usually involves taking a backup of the data and restoring it in a fresh database of the desired version. This is exactly what we will do.

First, we need to make sure all services using the database are stopped to prevent onging access to the postgresql instance.

In my case this means stopping the miniflux service:

docker-compose stop miniflux

Taking the backup can then be done using pg_dumpall:

docker-compose exec db pg_dumpall -U miniflux > dump.sql

Now we do not need the old database instance anymore and can stop/remove all container.

docker-compose down

The change of the docker-compose.yml are list below:

   db:
-    image: postgres:9.6
+    image: postgres:14
     restart: always
     environment:
       - POSTGRES_USER=miniflux
       - POSTGRES_PASSWORD=<db_password>
     volumes:
-      - miniflux-db:/var/lib/postgresql/data
+      - miniflux_postgres_14:/var/lib/postgresql/data

 volumes:
-  miniflux-db:
-    external: true
+  miniflux_postgres_14:
+    external: true

I prefer using external volumes to not accidentally remove the volumes by passing the -v flag to the docker-compose down command. But this means we have to manually create the new volume:

docker volume create miniflux_postgres_14
docker-compose up -d db

After the database is up, we can restore the data:

cp dump.sql /var/lib/docker/volumes/miniflux_postgres_14/_data/
docker-compose exec db bash
root@<container-id>:/# psql -U miniflux -d miniflux < /var/lib/postgresql/data/dump.sql

Unfortunately, we are not finished yet. As described in this blog post, newer versions of postgresql use a different password encryption scheme.

Affected users can be identified using the following sql:

SELECT
  rolname,
  rolpassword ~ '^SCRAM-SHA-256\$' AS has_upgraded
FROM pg_authid
WHERE rolcanlogin;

For all users for which the has_upgraded column is f (false), the password has to be set again. You can use the same password as before.

root@<container-id>:/# psql -U miniflux
[..]
miniflux=# SELECT
  rolname,
  rolpassword ~ '^SCRAM-SHA-256\$' AS has_upgraded
FROM pg_authid
WHERE rolcanlogin;

 rolname  | has_upgraded
----------+--------------
 miniflux | f
(1 row)

miniflux=# \password
Enter new password:
Enter it again:

Now we can start all services again and cleanup all backup dumps and unused volumes

docker-compose up -d
rm /var/lib/docker/volumes/miniflux_postgres_14/_data/dump.sql
docker volume remove miniflux-db

Even though these are some manual steps, I am very happy with how far you can go with a simple docker-compose setup.

docker