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/ or mirror.gcr.io/: 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/ 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/. The 19 image looks for its data in /var/, 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/ with a volume at /var/, 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/ 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/; with PostgreSQL 17 data in it, the path line read /var/. 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-bookwormThen 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
Stop every service that writes to the database, leave the pinned 18 container running, and write everything to a file. Replace
appwith the names of your other services (docker compose config --serviceslists them), use yourPOSTGRES_USERinstead ofpostgresif your file sets one, and your service's name instead ofdb:docker compose stop app docker compose exec -T db pg_dumpall -U postgres > all.sql docker compose downStop 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.In the Compose file, set
image: postgres:19and point the service at a new, empty volume mounted at/var/lib/postgresql, for example- pgdata19:/, withvar/ lib/ postgresql pgdata19:added under the top-levelvolumes:. If your file mounted/var/or setlib/ postgresql/ data PGDATA, drop both now: one mount at/var/lib/postgresqlis the layout that makes the next upgrade easier. Keep the old volume until your app works on 19.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 -dStarting 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 yourPOSTGRES_USERif you set one), and, if your file setsPOSTGRES_USERorPOSTGRES_DB, alsodatabase "…" 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/18Save them as Dockerfile in a new, empty folder, for example ~/pg-upgrade. Then work from the folder with your Compose file:
Back up (a
pg_dumpallfile as in step 1 above is enough). After--link, once 19 has started on the data, 18 can no longer use it.Stop the stack with
docker compose down(no-v), so 18 shuts down cleanly.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-upgradeThen run the upgrade on the volume. Compose names it
<project>_<volume>;docker volume lslists it. For a bind mount such as./pgdata, use the host folder instead of the volume name, for example-v "$PWD/, and first give the folder to thepgdata":/ var/ lib/ postgresql postgresuser, orinitdbstops withcould not create directory "19": Permission denied:docker run --rm -v "$PWD/pgdata":/var/lib/postgresql pg-upgrade-18-19 chown postgres:postgres /var/lib/postgresqlFor 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/'-Umust be the user that created the database (yourPOSTGRES_USER, elsepostgres). If your stack setsPOSTGRES_INITDB_ARGS, add the same options toinitdb;pg_upgradechecks that the two clusters match. Thecpkeeps the 18 access rules: the image's entrypoint adds a password rule for other containers when it creates a database, and a hand-runinitdbdoes not.Set
image: postgres:19on the same volume and rundocker compose up -d. The 19 image finds its data in19/dockerand starts. Then refresh the planner statistics, aspg_upgradeadvises:docker compose exec -T db vacuumdb -U postgres --all --analyze-in-stages --missing-stats-onlyHere too, use your
POSTGRES_USERinstead ofpostgresif you set one.pg_upgradealso suggests a slowervacuumdb … --all --analyze-only, which we did not run. It leaves adelete_old_cluster.shin 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_upgradeon Alpine images or on thebookwormvariant; 19's ownpg_dumpallagainst 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.
| Image | Digest |
|---|---|
postgres:19rc1 (Debian trixie) | sha256:b3cb7405059fd06f9ebae4aad4780b3788dfdd2acef1ac248ad9d56ced8abb06 |
postgres:19rc1-alpine | sha256:a3095ec8782b77587ca34006edaf09df362212351745069cb7758e256ecd488f |
postgres:18 (18.6, Debian trixie) | sha256:979bccfde35445db618c65107d27d08c6c21339d8dc3ad4c830c6633947be0e4 |
postgres:18-alpine (18.6) | sha256:10a5c69a83035ab5239f5620d708520e8b28f5aa1f2b20143e6024532edadfb5 |
postgres:17 (17.11) | sha256:b8faeff7712663c2773a190181e1246e37e364b887611c04658c28b44e2408fe |
| Starting point | postgres:19rc1 on it | Back on the old version | Upgrade to 19 RC1 |
|---|---|---|---|
One volume at /var/lib/postgresql, 18 data (Debian and Alpine) | Error: in 18+ … /var/, exit 1 | postgres:18 (Debian), postgres:18-alpine (Alpine): 1,000 of 1,000 rows, same checksum | Dump 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=, 18 data (Debian) | FATAL: database files are incompatible with server, exit 1 | postgres:18: 1,000 of 1,000 | Dump and restore into one volume at /var/lib/postgresql: 1,000 of 1,000 |
Volume at /var/, empty | Error: in 18+ … (unused mount/volume), exit 1 | – | – |
Volume at /var/, 17 data | Error: in 18+ … /var/, exit 1 | postgres:17: 1,000 of 1,000 | – |
Compose, restart: unless-stopped, 18 data | Started, exit status 0 from up -d; 20 seconds later ps: Up 2 seconds or Restarting (1); 8 restarts, exit 1 | postgres:18: 1,000 of 1,000 | Dump 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.