FATAL: database files are incompatible with server — a postgres:18
container has been handed a data directory that PostgreSQL 16 created, and it refuses to
open it. A server cannot read another major version's files. Move the data across with a
dump and restore, or with pg_upgrade, into a fresh PostgreSQL 18 volume.
The quick route: start the old volume with postgres:16 again, start
postgres:18 on a new empty volume mounted at /var/lib/postgresql,
and pipe pg_dumpall from the first into psql on the second.
The error
$ docker run -d --name fix3-dpg-pg18b -e POSTGRES_PASSWORD=dev-only-password -e PGDATA=/var/lib/postgresql/data -v fix3-dpg-old:/var/lib/postgresql/data postgres:18
$ docker logs fix3-dpg-pg18b
PostgreSQL Database directory appears to contain a database; Skipping initialization
2026-10-02 01:23:28.741 UTC [1] FATAL: database files are incompatible with server
2026-10-02 01:23:28.741 UTC [1] DETAIL: The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 18.4 (Debian 18.4-1.pgdg13+1).
Why it happens
Each PostgreSQL major version has its own on-disk format, and the data directory records
which one wrote it: the PG_VERSION file in this volume says 16.
The 18 server reads it at startup and stops. Changing the image tag changes the server,
never the files.
The postgres:18 image tries to stop you earlier. With the old volume still
mounted at /var/lib/postgresql/data, or moved one level up, it exits with a
different message about pg_ctlcluster and major-version directories. You
reach this one by going around that guard: setting PGDATA back to the old
path, as above, or mounting the old volume straight at the new path,
/var/lib/postgresql/18/docker. Both produced exactly this text. In both cases
the entrypoint sees an existing database, skips initialisation and starts an 18 server
on 16 files.
The fix
docker run -d --name fix3-dpg-pg16 -e POSTGRES_PASSWORD=dev-only-password -v fix3-dpg-old:/var/lib/postgresql/data postgres:16
docker run -d --name fix3-dpg-pg18 -e POSTGRES_PASSWORD=dev-only-password -v fix3-dpg-new:/var/lib/postgresql postgres:18
docker exec fix3-dpg-pg16 pg_dumpall -U postgres | docker exec -i fix3-dpg-pg18 psql -U postgres
docker exec fix3-dpg-pg18 psql -U postgres -d shop -c "SELECT version();" -c "SELECT id, name, email FROM customers ORDER BY id;"
The restore printed ERROR: role "postgres" already exists, which is harmless:
the new cluster already has that role, and the next statement gives it the old cluster's
password. All four rows came back on 18.4, accents intact. The pipe was run from cmd and
from Git Bash with the same result.
The alternative is pg_upgrade, which needs the binaries of both versions. The
18 image does not include 16, so a throwaway container installs them from the PostgreSQL
apt repository the image already lists (network needed). The new cluster must be
initialised without data checksums: a postgres:18 volume created with the
defaults had them on, the 16 one did not, and pg_upgrade refused with
old cluster does not use data checksums but the new one does:
docker run -d --name fix3-dpg-up18b -e POSTGRES_PASSWORD=dev-only-password -e POSTGRES_INITDB_ARGS=--no-data-checksums -v fix3-dpg-up2:/var/lib/postgresql postgres:18
docker stop fix3-dpg-up18b
docker rm fix3-dpg-up18b
docker run --rm --name fix3-dpg-upg3 -v fix3-dpg-up2:/var/lib/postgresql -v fix3-dpg-old:/var/lib/postgresql/16/docker postgres:18 bash -c "apt-get update -qq && apt-get install -y -qq postgresql-16 >/dev/null 2>&1 && cd /tmp && gosu postgres pg_upgrade -b /usr/lib/postgresql/16/bin -B /usr/lib/postgresql/18/bin -d /var/lib/postgresql/16/docker -D /var/lib/postgresql/18/docker"
It ended with Upgrade Complete, and postgres:18 on that volume
returned the same rows. Stop the old container first; pg_upgrade starts each server
itself.
How it was reproduced
Docker Desktop 4.91.0 (engine 29.8.0) on Windows 11, images postgres:16
(16.14) and postgres:18 (18.4). A 16 container on a named volume at
/var/lib/postgresql/data got a shop database with four fictional
customers. The same volume under postgres:18 gave the guard message, then
this error with PGDATA pointed back at it, and again when a copy of it was
mounted at /var/lib/postgresql/18/docker. The pg_upgrade container installed 16.15.
--link across two volumes failed with Invalid cross-device link;
with both clusters in one volume it completed. Names beginning fix3-dpg- are
this test's own containers and volumes; use yours.
Frequently asked
- How do I run pg_upgrade from PostgreSQL 16 to 18 in Docker?
- Stop the old container. Initialise a new postgres:18 volume with POSTGRES_INITDB_ARGS=--no-data-checksums, then run a throwaway postgres:18 container with both volumes mounted, install postgresql-16 with apt-get, and run pg_upgrade as the postgres user with -b, -B, -d and -D pointing at the two versions.
- Why does pg_upgrade --link fail in Docker with Invalid cross-device link?
- Link mode hard-links files instead of copying them, so the old and new data directories must be on the same file system. Two Docker volumes are two file systems. Put both clusters in one volume mounted at /var/lib/postgresql, as 16/docker and 18/docker, or drop --link and let pg_upgrade copy.
- Where do I mount the volume for postgres 18, and what is /var/lib/postgresql/18/docker?
- Mount the volume at /var/lib/postgresql. The postgres:18 image keeps its data in the 18/docker subdirectory of that mount. Do not mount an old PostgreSQL 16 volume straight at /var/lib/postgresql/18/docker; that hands 16 files to an 18 server and produces this error.
More decoded errors in the Fixes category. If your
postgres:18 container exits with the pg_ctlcluster message instead,
see postgres:18 container
exits immediately.