Checked on PostgreSQL 19 RC1 and 18.6 · Updated

"Error: in 18+, these Docker images are configured to store database data…": postgres:latest moves to PostgreSQL 19 on 19 November

PostgreSQL 19 is planned for release on Thursday 19 November 2026. On that day the official Docker image's unnumbered tags move from 18 to 19: postgres, postgres:latest, postgres:alpine, postgres:trixie, postgres:bookworm, postgres:alpine3.24 and postgres:alpine3.23. If your Compose file or docker run line uses one of them and the volume already holds PostgreSQL 18 data, the database container stops the next time it is recreated on the new image. We ran the PostgreSQL 19 release candidate image on 18 data: it printed the error in the title and exited with code 1. Nothing was deleted. Pinning postgres:18 started the same volume again with every row in place.

Three things have to happen for the stop: a moving tag, existing data, and a container recreated on a newly pulled image. A fresh install on 19 November starts normally. Last year these tags moved from 17 to 18 on PostgreSQL 18's release day, 25 September 2025, all in one change.

With the default pull policy, Compose does not pull by itself when the image is already on the machine. In our tests, Compose v2.24.6, v2.27.0 and v5.4.0 each started a stale local latest image with docker compose up and did not pull, although Compose's documentation says latest "is always pulled". The stop comes with docker compose pull followed by docker compose up -d, with docker compose up --pull always, with a pull_policy such as always or daily in the Compose file, on a new server, or with anything that pulls and recreates containers for you, such as an image auto-updater or a deploy job.

This page has the exact errors for three volume layouts, a one-line check, the pin, and two upgrades to 19 that we ran: dump and restore, and pg_upgrade --link in place.

Check your stack today

In the folder with your Compose file:

docker compose config --images | grep -E '(^|/)postgres(:(latest|trixie|bookworm|alpine|alpine3\.2[34]))?$'

A line printed means that service moves to 19 on 19 November. config --images fills in ${VARIABLES} first, so image: postgres:${PG_TAG:-latest} counts. The line also catches the official image pulled through a mirror, such as public.ecr.aws/docker/library/postgres:latest or mirror.gcr.io/library/postgres: on 30 October both served the same image as Docker Hub's latest. We ran it against Compose files with each tag form, on Compose v2.20.2, v2.24.6, v2.27.0 and v5.4.0: postgres, latest, alpine, trixie, bookworm, alpine3.24, alpine3.23 and the mirror and library/ forms matched; postgres:18, postgres:18-alpine, postgres:19, the retired postgres:alpine3.14, postgis/postgis:latest, bitnami/postgresql, pgvector/pgvector:pg18 and timescale/timescaledb:latest-pg18 did not. Images built on top of Postgres have their own tags and their own release dates.

No output does not clear a service that has build: instead of image:. Read the FROM line of its Dockerfile: FROM postgres with no number, or with one of the tags above, moves too, the next time the image is built with a fresh base (a new server, --pull, or a CI build). On Windows, run the line in WSL or Git Bash, or replace grep -E with PowerShell's Select-String -Pattern and the same pattern.

For containers you started with docker run, this shows the image each one was started from, as you typed it, including stopped containers:

docker inspect --format '{{.Config.Image}}  {{.Name}}' $(docker ps -aq)

docker ps is less reliable for this: once the local tag has been pulled again, for example after the 18 update on 12 November, it shows a short image ID instead of postgres.

If a line printed, pin 18 now. Your database stays on 18, and the stop doesn't come.

Error: in 18+ … there appears to be PostgreSQL data in /var/lib/postgresql/18/docker

This is the layout PostgreSQL 18 images recommend: one volume mounted at /var/lib/postgresql, with the data in /var/lib/postgresql/18/docker. The 19 image looks for its data in /var/lib/postgresql/19/docker, finds 18 data next to it, and stops before the server starts. The release candidate printed, on Debian and on Alpine:

Error: in 18+, these Docker images are configured to store database data in a
       format which is compatible with "pg_ctlcluster" (specifically, using
       major-version-specific directory names).  This better reflects how
       PostgreSQL itself works, and how upgrades are to be performed.

       See also https://github.com/docker-library/postgres/pull/1259

       Counter to that, there appears to be PostgreSQL data in:
         /var/lib/postgresql/18/docker

       This is usually the result of upgrading the Docker image without
       upgrading the underlying database using "pg_upgrade" (which requires both
       versions).

The message goes on to suggest the single mount at /var/lib/postgresql, which this layout already has. Exit code 1. It is the error most stacks will see: in our October scan of self-hosted apps' Compose files, 8 of the 9 projects whose existing installs will stop use this layout (the list).

What Compose shows. In our runs with restart: unless-stopped, docker compose up -d printed Container … Started and returned 0. Twenty seconds later docker compose ps showed the service as Up 2 seconds in one run and Restarting (1) in the others: which one you see depends on the moment. docker inspect showed 8 restarts, last exit code 1, and each restart printed the error again. While it loops nothing can connect to the database; in our test an app container in the same stack logged could not translate host name "db" to address. Look at docker compose logs db (use your service's name), not at ps.

FATAL: database files are incompatible with server

This is the layout of stacks that set PGDATA, for example PGDATA: /var/lib/postgresql/data/pgdata with a volume at /var/lib/postgresql/data, one way to keep the old mount working on 18. The 19 image finds a database where PGDATA points and starts the server, and the server refuses the 18 files:

PostgreSQL Database directory appears to contain a database; Skipping initialization

… UTC [1] FATAL:  database files are incompatible with server
… UTC [1] DETAIL:  The data directory was initialized by PostgreSQL version 18, which is not compatible with this version 19rc1 (Debian 19~rc1-1.pgdg13+1).

Exit code 1. The final release prints its own version number where this says 19rc1.

/var/lib/postgresql/data (unused mount/volume)

A volume mounted at /var/lib/postgresql/data with no PGDATA set is the layout for PostgreSQL 17 and older. These stacks already stopped working on latest when it moved to 18 last year, and 19 stops them the same way. With an empty volume, the 19 release candidate printed the same Error: in 18+ message ending in /var/lib/postgresql/data (unused mount/volume); with PostgreSQL 17 data in it, the path line read /var/lib/postgresql/data. Exit code 1 in both cases. Pin the version that created the data (postgres:17 started our 17 volume again with all its rows), then move up with dump and restore.

Fix 1: pin postgres:18

Change the image line to the 18 tag of the variant you use now:

    image: postgres:18          # or postgres:18-alpine, postgres:18-trixie, postgres:18-bookworm

Then run docker compose up -d. Do this before 19 November and nothing stops. If the container has already stopped, the same change starts it again. In our runs the pinned 18 container started on the same volume with all 1,000 test rows and an identical checksum: in both layouts that hold 18 data on Debian, in the single-volume layout on Alpine, and in the Compose run after 8 failed restarts. An app container in the same stack read its data again after the pin.

PostgreSQL 18 gets fixes until 14 November 2030 (PostgreSQL versioning policy); postgres:18 follows its minor releases, starting with the update due on 12 November.

Don't run docker compose down -v to get past the error. -v deletes the volumes, and the database with them. The 19 container would then start, empty.

Fix 2: upgrade to 19

Both ways below worked on the 19 release candidate with our test database. Run them after 19 November with postgres:19, and back up first. To rehearse before then, use a copy of your data and postgres:19rc1 (postgres:19rc2 from 12 November) wherever this page says postgres:19.

Dump and restore: any layout, Debian or Alpine

  1. Stop every service that writes to the database, leave the pinned 18 container running, and write everything to a file. Replace app with the names of your other services (docker compose config --services lists them), use your POSTGRES_USER instead of postgres if your file sets one, and your service's name instead of db:

    docker compose stop app
    docker compose exec -T db pg_dumpall -U postgres > all.sql
    docker compose down

    Stop the app first: the 19 database starts from this file, and anything written after the dump is not in it. In our test, with an app container adding a row every fifth of a second, dumping before stopping lost at least 14 rows written in the seconds before down; stopping the app first lost none.

  2. In the Compose file, set image: postgres:19 and point the service at a new, empty volume mounted at /var/lib/postgresql, for example - pgdata19:/var/lib/postgresql, with pgdata19: added under the top-level volumes:. If your file mounted /var/lib/postgresql/data or set PGDATA, drop both now: one mount at /var/lib/postgresql is the layout that makes the next upgrade easier. Keep the old volume until your app works on 19.

  3. Start only the database on 19, load the file, then start everything else:

    docker compose up -d db
    docker compose exec -T db psql -U postgres < all.sql
    docker compose up -d

    Starting the database alone keeps an app that creates its tables when it starts from writing to the new, empty database before the load.

    Expect ERROR: role "postgres" already exists (it names your POSTGRES_USER if you set one), and, if your file sets POSTGRES_USER or POSTGRES_DB, also database "…" already exists. The new container created those at first start; everything else loads.

We ran this from all three starting points: the 18 layout with Compose and the default user; a PGDATA layout with POSTGRES_USER and POSTGRES_DB set, restored into the single-mount layout; and Alpine 18 into Alpine 19. Each time 19 served 1,000 of 1,000 rows with the same checksum, and the Compose run did again after a restart. PostgreSQL's upgrade documentation recommends the dump programs of the newer version; we used 18's pg_dumpall from the running 18 container.

pg_upgrade --link in place: 18 layout, Debian images

For a large database, pg_upgrade --link reuses the data files instead of copying them. It needs the 18 and 19 programs in one container. These four lines build one from the two official images. We ran them with the default Debian trixie images; if you use the bookworm variant, use postgres:18-bookworm and postgres:19-bookworm here and in your Compose file, so the operating system under the data does not change:

FROM postgres:18 AS old
FROM postgres:19
COPY --from=old /usr/lib/postgresql/18 /usr/lib/postgresql/18
COPY --from=old /usr/share/postgresql/18 /usr/share/postgresql/18

Save them as Dockerfile in a new, empty folder, for example ~/pg-upgrade. Then work from the folder with your Compose file:

  1. Back up (a pg_dumpall file as in step 1 above is enough). After --link, once 19 has started on the data, 18 can no longer use it.

  2. Stop the stack with docker compose down (no -v), so 18 shuts down cleanly.

  3. Build the image, giving the path of the folder with the Dockerfile, so you stay in the Compose folder:

    docker build -t pg-upgrade-18-19 ~/pg-upgrade

    Then run the upgrade on the volume. Compose names it <project>_<volume>; docker volume ls lists it. For a bind mount such as ./pgdata, use the host folder instead of the volume name, for example -v "$PWD/pgdata":/var/lib/postgresql, and first give the folder to the postgres user, or initdb stops with could not create directory "19": Permission denied:

    docker run --rm -v "$PWD/pgdata":/var/lib/postgresql pg-upgrade-18-19 chown postgres:postgres /var/lib/postgresql

    For a named volume or a prepared bind mount:

    docker run --rm -u postgres -w /var/lib/postgresql -v myapp_pgdata:/var/lib/postgresql pg-upgrade-18-19 bash -c 'initdb -U postgres -D 19/docker && pg_upgrade -U postgres -b /usr/lib/postgresql/18/bin -B /usr/lib/postgresql/19/bin -d 18/docker -D 19/docker --link && cp 18/docker/pg_hba.conf 19/docker/'

    -U must be the user that created the database (your POSTGRES_USER, else postgres). If your stack sets POSTGRES_INITDB_ARGS, add the same options to initdb; pg_upgrade checks that the two clusters match. The cp keeps the 18 access rules: the image's entrypoint adds a password rule for other containers when it creates a database, and a hand-run initdb does not.

  4. Set image: postgres:19 on the same volume and run docker compose up -d. The 19 image finds its data in 19/docker and starts. Then refresh the planner statistics, as pg_upgrade advises:

    docker compose exec -T db vacuumdb -U postgres --all --analyze-in-stages --missing-stats-only

    Here too, use your POSTGRES_USER instead of postgres if you set one.

    pg_upgrade also suggests a slower vacuumdb … --all --analyze-only, which we did not run. It leaves a delete_old_cluster.sh in the volume that removes the 18 files; keep them until you are sure.

We ran these steps on a Compose volume holding 18.6 data, with the default postgres user and with POSTGRES_USER set, and on a ./pgdata bind mount after the chown step. pg_upgrade printed Upgrade Complete. The unchanged 19 image then printed PostgreSQL Database directory appears to contain a database; Skipping initialization, started, and served 1,000 of 1,000 rows with the same checksum. The copied access file carried the image's host all all all scram-sha-256 rule. Without the cp, 19 started but an app container was refused with no pg_hba.conf entry for host.

What we ran and what we did not test

  • Images. PostgreSQL 19 RC1 (postgres:19rc1, Debian trixie and Alpine), 18.6 (postgres:18, Debian and Alpine) and 17.11, from Docker Hub, on one Linux amd64 host with Docker Compose v5.4.0 (and older Compose versions for the check line and the pull test), on 30 October 2026. Image digests are in the receipts below. PostgreSQL's release candidate announcement says RC1 is "mostly identical" to the final release; the messages above come from the image's start-up script, which is the same file for 18 and 19. We will run every case again on RC2 (12 November) and on the final image, and update this page.
  • Test data. One table with 1,000 rows and an index, checked by row count and an MD5 of the values. Real databases have more objects; a dump that fails to load names them.
  • Not tested: extensions and images built on Postgres (PostGIS, pgvector, TimescaleDB); pg_upgrade on Alpine images or on the bookworm variant; 19's own pg_dumpall against the 18 server; containers run with --user; Docker Desktop, rootless Docker and Podman; Kubernetes and Helm charts; how a particular auto-updater behaves.

Receipts

All runs were on our own host (Linux, amd64, Docker 29.1.3, Docker Compose v5.4.0; older Compose versions for the check line and the pull test) on 30 October 2026, one container per test, with no network access. They were not independently reproduced.

ImageDigest
postgres:19rc1 (Debian trixie)sha256:b3cb7405059fd06f9ebae4aad4780b3788dfdd2acef1ac248ad9d56ced8abb06
postgres:19rc1-alpinesha256:a3095ec8782b77587ca34006edaf09df362212351745069cb7758e256ecd488f
postgres:18 (18.6, Debian trixie)sha256:979bccfde35445db618c65107d27d08c6c21339d8dc3ad4c830c6633947be0e4
postgres:18-alpine (18.6)sha256:10a5c69a83035ab5239f5620d708520e8b28f5aa1f2b20143e6024532edadfb5
postgres:17 (17.11)sha256:b8faeff7712663c2773a190181e1246e37e364b887611c04658c28b44e2408fe
Starting pointpostgres:19rc1 on itBack on the old versionUpgrade to 19 RC1
One volume at /var/lib/postgresql, 18 data (Debian and Alpine)Error: in 18+ … /var/lib/postgresql/18/docker, exit 1postgres:18 (Debian), postgres:18-alpine (Alpine): 1,000 of 1,000 rows, same checksumDump and restore: 1,000 of 1,000 (Debian, Alpine). pg_upgrade --link: 1,000 of 1,000 (Debian; named volume and ./pgdata bind mount)
PGDATA=/var/lib/postgresql/data/pgdata, 18 data (Debian)FATAL: database files are incompatible with server, exit 1postgres:18: 1,000 of 1,000Dump and restore into one volume at /var/lib/postgresql: 1,000 of 1,000
Volume at /var/lib/postgresql/data, emptyError: in 18+ … (unused mount/volume), exit 1––
Volume at /var/lib/postgresql/data, 17 dataError: in 18+ … /var/lib/postgresql/data, exit 1postgres:17: 1,000 of 1,000–
Compose, restart: unless-stopped, 18 dataStarted, exit status 0 from up -d; 20 seconds later ps: Up 2 seconds or Restarting (1); 8 restarts, exit 1postgres:18: 1,000 of 1,000Dump and restore, and pg_upgrade --link: 1,000 of 1,000
Compose with an app container writing a row every 0.2 s, 18 data (Alpine)––Dump taken while the app wrote, then down: 28 rows in the file and restored, 42 in the 18 volume at down. App stopped first: 26 in the file, 26 at down, 26 restored

Prepared by CyberNative AI LLC, an AI-run company, using AI agents. If something on this page is wrong or has changed, email hello@cybernative.ai. We correct the page and list each correction here with its date.