Upgrading PostgreSQL, however you installed it
This one started life as a draft back in the Postgres 12 days, when the fix for a major-version upgrade was a single magic Homebrew command. That command no longer exists, and these days Postgres arrives on my machines three or four different ways anyway, so here’s the full 2026 tour: the universal idea, then the specifics for each install method.
The error, and the idea
Whatever route you installed by, the failure looks the same. Postgres refuses to start and the log says:
FATAL: database files are incompatible with server
DETAIL: The data directory was initialized by PostgreSQL version 16,
which is not compatible with this version 17.
Postgres major versions each have their own on-disk data format, so a new server can’t read an old data directory. Every upgrade method below is really just one of two moves:
- Dump and restore:
pg_dumpallfrom the old server, replay into the new one. Simple, reliable, fine for the database sizes on a dev machine. pg_upgrade: converts the data directory in place. Much faster for big databases, fussier to run because it needs both versions’ binaries at once.
For a laptop, dump and restore wins almost every time:
# with the OLD version still running
pg_dumpall > ~/pg_backup.sql
Keep that file safe, and every section below ends the same way:
psql -d postgres -f ~/pg_backup.sql
Homebrew
Once upon a time you ran brew postgresql-upgrade-database and went for a cuppa. Homebrew removed that command when it moved to versioned-only formulae (postgresql@16, postgresql@17 and so on), so if an old blog post (including the draft this one grew from) tells you to run it, the post has expired.
The versioned formulae do make things cleaner though, since old and new coexist:
brew services start postgresql@16 # old one up, take the dump
pg_dumpall > ~/pg_backup.sql
brew services stop postgresql@16
brew install postgresql@17
brew services start postgresql@17
psql -d postgres -f ~/pg_backup.sql
Check everything arrived with psql -l, fix your PATH to point at the new version (Homebrew prints the line you need), then brew uninstall postgresql@16 when you’re confident.
Postgres.app
If you use Postgres.app, upgrades are its party trick. Each server it manages is a version plus a data directory, and you can run several at once on different ports. Create a new server with the new version from the sidebar, start it on port 5433 alongside the old one on 5432, then pipe one into the other directly, no intermediate file needed:
pg_dumpall -p 5432 | psql -p 5433 -d postgres
Point your apps at the new port (or stop the old server and move the new one to 5432), and delete the old server from the sidebar when you’re done.
mise and asdf
If you’ve read my mise post, you know I like one tool managing everything, and yes, that can include Postgres. Both mise and asdf install it via the asdf postgres plugin, which compiles from source, so expect the install step to take a while:
# mise
mise install postgres@17
mise use -g postgres@17
# asdf
asdf plugin add postgres
asdf install postgres 17.5
asdf global postgres 17.5
The key thing to understand: each installed version keeps its own separate data directory inside the tool’s install folder. That makes the upgrade mechanically simple (versions never fight over data) but also means a version switch is an empty new server, not a migrated one. So: dump from the old version while it’s still active, switch versions, initdb if the plugin hasn’t already, start the new server with pg_ctl start, and restore.
Honest verdict: I love mise for Ruby and Node, where versions switch per-project constantly. Postgres isn’t like that; you almost always want one server running as a service. On a Mac I’d still let Homebrew or Postgres.app own Postgres, and keep mise for languages.
Docker
If your Postgres lives in a container, the image tag is the version, and the volume is the data directory, so the incompatibility rule applies to the volume. Bumping postgres:16 to postgres:17 against the same volume gives you the exact FATAL error from the top of this post.
The clean route is the same dance with containers:
docker exec old_pg pg_dumpall -U postgres > ~/pg_backup.sql
docker compose down
# bump the image tag to postgres:17 and point it at a NEW volume
docker compose up -d
docker exec -i new_pg psql -U postgres < ~/pg_backup.sql
There’s also the pgautoupgrade image, which runs pg_upgrade on your volume at container start. Handy for homelab setups, but I’d still take the dump first. Always take the dump first.
Linux, for completeness
On Debian and Ubuntu the distro tooling genuinely has this solved: sudo pg_upgradecluster 16 main migrates a cluster across versions and keeps the old one renamed alongside until you drop it. The one place the magic one-liner still exists.
The developer’s shortcut
Final honest aside: on a dev machine where every database is a Rails app that can rebuild itself, sometimes the quickest upgrade is not migrating data at all. Install the new version fresh, then in each project run rails db:setup. Schema, seeds, done. “Restore from backup” and “regenerate from code” are both valid answers; only you know whether your local data is precious or disposable.