Self-hosted service
tianon/docker-postgres-upgrade avatar
tianon/docker-postgres-upgrade

tianon/docker-postgres-upgrade: Docker Packaging for pg_upgrade

a PoC for using "pg_upgrade" inside Docker -- learn from it, adapt it for your needs; don't expect it to work as-is!

1,387 stars127 forksShellMIT

At a glance

What is it?
tianon/docker-postgres-upgrade packages PostgreSQL's pg_upgrade workflow into version-specific Docker images. It suits engineers who can adapt a proof of concept, but the repository explicitly warns that the commands are not ready to use unchanged.
Who is it for?
Adopt this image when your major PostgreSQL upgrade follows its directory layout and you are prepared to inspect the generated configuration. Do not treat it as a turnkey migration service.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 71 days ago.
What is it written in?
Mainly Shell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why tianon/docker-postgres-upgrade Exists

tianon/docker-postgres-upgrade addresses a specific Docker problem: running PostgreSQL's pg_upgrade against an old data directory and a new one during a major-version upgrade. The example coordinates PostgreSQL data paths and mounted storage to run pg_upgrade across major versions. This project wraps that work in images named OLD-to-NEW, such as 17-to-18.

The intended audience is an engineer who already understands PostgreSQL major upgrades and is willing to adapt a proof of concept. The README does not describe it as a production migration product. Its warning is unusually direct: "learn from it, adapt it for your needs; don't expect it to work as-is!" That boundary matters. A team looking for a one-command service with documented backup, rollback, extension compatibility, and recovery checks would find too much of that work left outside this repository.

The project solves container packaging and path coordination rather than database migration policy. It does not decide whether the upgrade is safe for a given database. It also does not document a rollback procedure. What it contributes is a repeatable shell and Docker structure around pg_upgrade, plus concrete volume layouts that connect the old PostgreSQL image to the layout expected by the new image.

The OLD-to-NEW Image and Data Layout

The repository uses one top-level directory for each supported source and destination pair. The top-level entries include 14-to-15, 14-to-16, 14-to-17, 14-to-18, 15-to-16, 15-to-17, 15-to-18, 16-to-17, 16-to-18, and 17-to-18. This organization indicates that image generation is driven by templates and a defined version matrix rather than one image that discovers every supported PostgreSQL pair.

The preferred host structure places each cluster two levels below a common directory:

bash
$ find DIR -mindepth 2 -maxdepth 2
DIR/OLD/docker
DIR/NEW/docker

That structure allows one mount to expose both clusters at `/var/lib/postgresql`. Inside the upgrade container, PGDATAOLD points to `/var/lib/postgresql/OLD/docker`, while PGDATANEW points to `/var/lib/postgresql/NEW/docker`. The repository also includes `Dockerfile.template`, `apply-templates.sh`, `update.sh`, `versions.sh`, and `versions.json`. Based on that layout, the version directories are generated or maintained image recipes, while the shell and JSON files coordinate versions and templates. The README does not explain those generation scripts in enough detail to document their individual inputs or execution order.

Running pg_upgrade Through Linked Containers

The README recommends `docker run image --link` and refers to PostgreSQL's pg_upgrade documentation for the reason: the recommended mode calls for the old and new PostgreSQL servers to be linked. The concrete repository command uses Docker's legacy-style `--link` argument and passes both data locations through environment variables.

For an upgrade from PostgreSQL 17 to 18, the command is:

bash
$ docker run --rm \
	--mount 'type=bind,src=/mnt/bigdrive/postgresql,dst=/var/lib/postgresql' \
	--env 'PGDATAOLD=/var/lib/postgresql/17/docker' \
	--env 'PGDATANEW=/var/lib/postgresql/18/docker' \
	tianon/postgres-upgrade:17-to-18 \
	--link

The bind mount makes the host's parent directory available inside the container at the path expected by the image. PGDATAOLD identifies the source cluster and PGDATANEW identifies the destination path. The image tag expresses the same transition.

This pattern depends on more than the two environment values. The old `postgres:17` instance is expected to have used `/mnt/bigdrive/postgresql/17/docker` as its `/var/lib/postgresql/data` mount. After upgrading, the new `postgres:18` instance receives the parent directory at `/var/lib/postgresql`, then sets PGDATA to the new cluster path. The repository cites docker-library/postgres#1259 to explain that parent-directory mount choice.

A team adopting an OLD-to-NEW pair must first confirm that the exact image tag exists. The repository contains version directories and has no GitHub releases, so the source tree suggests which transitions exist, but the README does not publish a complete current image-tag inventory.

Preparing a PostgreSQL 17-to-18 Practice Upgrade

The README's complete example creates a disposable host directory, starts PostgreSQL 17, initializes a data set with `pgbench`, stops that container, runs the upgrade image, and then starts PostgreSQL 18 against the new directory. It is an illustrative workflow rather than a production procedure.

First, create the working directory and define the source and destination versions exactly as shown:

bash
$ mkdir -p postgres-upgrade-testing
$ cd postgres-upgrade-testing
$ OLD='17'
$ NEW='18'

Start the source container with the bind mount, password, initialization arguments, and data directory used by the example:

bash
$ docker run -dit \
	--name postgres-upgrade-testing \
	--env POSTGRES_PASSWORD='password' \
	--env POSTGRES_INITDB_ARGS='--data-checksums' \
	--mount "type=bind,src=$PWD,dst=/var/lib/postgresql" \
	--env PGDATA="/var/lib/postgresql/$OLD/docker" \
	--pull always \
	"postgres:$OLD"

After allowing startup time, inspect the logs and load the documented `pgbench` data. These commands are suitable only as the README's practice setup; they are not evidence of production data volume or migration behavior.

bash
$ sleep 5
$ docker logs --tail 100 postgres-upgrade-testing

$ docker exec -it \
	-u postgres \
	postgres-upgrade-testing \
	pgbench -i -s 10

Stop and remove the source container before invoking the upgrade image. The README does not say what to do if the upgrade command fails, so its omission of a rollback workflow is a genuine operational limitation.

Running the 17-to-18 Upgrade and Starting Version 18

After stopping the source database, run the version-specific image against the same host directory. POSTGRES_INITDB_ARGS is retained in the upgrade invocation, while PGDATAOLD and PGDATANEW tell the image which clusters belong to the migration.

bash
$ docker stop postgres-upgrade-testing
$ docker rm postgres-upgrade-testing

$ docker run --rm \
	--env POSTGRES_INITDB_ARGS='--data-checksums' \
	--mount "type=bind,src=$PWD,dst=/var/lib/postgresql" \
	--env "PGDATAOLD=/var/lib/postgresql/$OLD/docker" \
	--env "PGDATANEW=/var/lib/postgresql/$NEW/docker" \
	--pull always \
	"tianon/postgres-upgrade:$OLD-to-$NEW" \
	--link

The README shows `--link` but does not provide a matching command that visibly creates the linked old and new server containers in this complete example. It instead points to PostgreSQL's pg_upgrade documentation for details. That makes the repository incomplete as a standalone tutorial for the linked mode.

Once the upgrade command returns, the documented next step creates a PostgreSQL 18 container. PGDATA now points at `/var/lib/postgresql/18/docker` within the parent mount:

bash
$ docker run -dit \
	--name postgres-upgrade-testing \
	--env POSTGRES_PASSWORD='password' \
	--env POSTGRES_INITDB_ARGS='--data-checksums' \
	--mount "type=bind,src=$PWD,dst=/var/lib/postgresql" \
	--env PGDATA="/var/lib/postgresql/$NEW/docker" \
	--pull always \
	"postgres:$NEW"
$ sleep 5
$ docker logs --tail 100 postgres-upgrade-testing

The README then says the old version can "probably" be removed. That word exposes the project's PoC status: it does not prescribe application validation, backup confirmation, or extension checks before deletion. Its optional cleanup command is:

bash
$ # can now (probably) safely remove "$OLD"
$ sudo rm -rf "$OLD"

Do not confuse the comment's placeholder description with permission to run that destructive command automatically. The README does not document rollback, restoration, or acceptance testing for the upgraded service.

The Separate-Mount Fallback and Its Trade-off

The image can run without the preferred parent-directory arrangement. In the alternate pattern, PGDATAOLD and PGDATANEW are each bound to `/var/lib/postgresql/OLD/docker` and `/var/lib/postgresql/NEW/docker`. This makes the source and destination appear to follow the expected internal structure even when their host directories are unrelated.

bash
$ docker run --rm \
	--mount 'type=bind,src=/mnt/bigdrive/postgresql-17,dst=/var/lib/postgresql/17/docker' \
	--mount 'type=bind,src=/mnt/bigdrive/postgresql-18,dst=/var/lib/postgresql/18/docker' \
	--env 'PGDATAOLD=/var/lib/postgresql/17/docker' \
	--env 'PGDATANEW=/var/lib/postgresql/18/docker' \
	tianon/postgres-upgrade:17-to-18

The README labels this approach slower and says it is not recommended for the new PostgreSQL container because that container would later mount only `/mnt/bigdrive/postgresql-18` at `/var/lib/postgresql/18/docker`. Its cited issue comment records that recommendation. The project therefore does not merely accept arbitrary paths. It maps them into a known layout as a fallback, while warning that this departs from the preferred persistent-volume arrangement.

A real alternative is to arrange pg_upgrade directly in a PostgreSQL environment tailored to the two versions, then start the destination server from the resulting directory. That approach avoids this image's tag matrix and template layout, but the repository provides no direct command for it. This image offers a reusable container contract around PGDATAOLD and PGDATANEW. Direct pg_upgrade offers fewer layers between an operator and the PostgreSQL documentation, but requires that operator to assemble the compatible binaries, server arrangement, mounts, and extension requirements.

Maintenance Scope, License, and Adoption Boundaries

The repository is not archived, and its last push was on 2026-08-13. It has no GitHub releases, so consumers cannot use the repository's release page as a version-history record. The presence of generated-looking version directories is useful for understanding source coverage, but the README does not state an update policy or guarantee that every Docker Hub tag tracks current changes immediately.

Upgrade cost falls mainly on the adopting engineer. There may be no package installation step in the usual sense because the artifact is a container image. The cost comes from reviewing the selected OLD-to-NEW recipe, matching existing Docker mounts, supplying both data paths, and validating the resulting cluster with the destination PostgreSQL image. Because the project invites adaptation, teams may also need to maintain changes they make to its templates or runtime workflow.

The repository is licensed under MIT. That is the stated licensing information for source use and modification, but operators remain responsible for separately checking the licenses and versions of PostgreSQL images, linked tools, databases, extensions, and any internal changes they introduce. This article does not provide legal advice.

The wrong choice is a fully managed migration tool with built-in backup, rollback, health checks, or application-level validation. The right choice is a small, inspectable proof of concept for a controlled PostgreSQL major-version upgrade where the operator already understands the surrounding constraints.

Editorial conclusion

Adopt this image when your major PostgreSQL upgrade follows its directory layout and you are prepared to inspect the generated configuration. Do not treat it as a turnkey migration service. Verify first that your selected OLD-to-NEW image exists, both data directories match the required structure, and your copy and recovery procedures are complete.

Frequently asked questions

How do I install tianon/docker-postgres-upgrade?

The README does not provide a local installation command. It directs users to the version-specific image on Docker Hub, such as `tianon/postgres-upgrade:17-to-18`, and provides `docker run` examples.

Which directory layout does tianon/docker-postgres-upgrade expect?

The preferred layout contains `OLD/docker` and `NEW/docker` beneath one parent directory. The README also documents separate host mounts that are mapped into those internal paths, but says this fallback will be slower.

Does tianon/docker-postgres-upgrade document rollback?

No. The README does not provide a rollback procedure or explain how to recover when an upgrade fails. It suggests removing the old version only after the new container has started.

Official sources

  1. Issues
  2. License: MIT
  3. Project website
  4. README
  5. tianon/docker-postgres-upgrade on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tianon-docker-postgres-upgrade.svg)](https://hysenlabs.com/projects/tianon-docker-postgres-upgrade)

Was this useful?