If you copy a Compose file from almost any PostgreSQL + Docker tutorial and change the tag to 18, the container stops a few seconds after it starts:
$ docker run -d --name pgold -e POSTGRES_PASSWORD=secret -v pgold:/var/lib/postgresql/data postgres:18
b480eb9c72b29fa99f1ed8df17972e2bfc2d6da4b86c058a36cf21fd63579bfa
{% raw %}$ docker ps -a --filter name=pgold --format "{{.Status}}"{% endraw %}
Exited (1) 5 seconds ago
Nothing is wrong with your password or your volume. The official image changed where it keeps the data. Everything below was run with postgres:18 (PostgreSQL 18.6) on Docker Engine 29.9.
What the log says
$ docker logs pgold 2>&1 | head -n 12
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/data (unused mount/volume)
This is usually the result of upgrading the Docker image without
upgrading the underlying database using "pg_upgrade" (which requires both
Up to version 17, the image used PGDATA=/var/lib/postgresql/data, so everyone mounted their volume there. From 18 on, the default is a version-specific directory:
| Image | Default data directory | Where to mount the volume |
|---|---|---|
postgres:17 and older |
/var/lib/postgresql/data |
/var/lib/postgresql/data |
postgres:18 |
/var/lib/postgresql/18/docker |
/var/lib/postgresql |
With the old mount, the real data directory would sit outside your volume, inside the container's writable layer, and disappear the next time you recreate the container. The entrypoint notices the unused mount and refuses to start. Annoying, but it stops you from losing data silently.
The fix
Mount the parent directory instead:
$ docker run -d --name pg -e POSTGRES_PASSWORD=secret -p 127.0.0.1:5432:5432 -v pgdata:/var/lib/postgresql postgres:18
$ docker exec pg psql -U postgres -c "SHOW data_directory;"
data_directory
-------------------------------
/var/lib/postgresql/18/docker
(1 row)
In Compose:
services:
db:
image: postgres:18
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql
volumes:
pgdata:
Because the directory name includes the major version, a future major version can live next to the old one in the same volume, which is what the change is designed for.
Upgrading an existing 16 or 17 database
Changing the tag on an existing database doesn't work either, for a different reason: data files are not compatible across major versions. Pointing 18 at a 16 data directory (forcing the old path with PGDATA) gives:
$ docker run --name pg18 -e POSTGRES_PASSWORD=secret -e PGDATA=/var/lib/postgresql/data -v pg16data:/var/lib/postgresql/data postgres:18 2>&1 | tail -n 3
2026-10-13 00:54:09.740 UTC [1] FATAL: database files are incompatible with server
2026-10-13 00:54:09.740 UTC [1] DETAIL: The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 18.6 (Debian 18.6-1.pgdg13+2).
The simplest safe path is dump and restore into a new volume, keeping the old one untouched until you've checked the result:
$ docker exec pg16 pg_dumpall -U postgres > upgrade.sql
$ docker run -d --name pg18 -e POSTGRES_PASSWORD=secret -v pg18data:/var/lib/postgresql postgres:18
$ docker exec -i pg18 psql -U postgres -q < upgrade.sql
ERROR: role "postgres" already exists
That one error is expected: the new container already created the postgres role, and the dump tries to create it again. Everything else is restored. Check the version and your data, then switch your app to the new container.
Two things to watch on Windows: PowerShell doesn't support < for input, and Windows PowerShell 5.1's > writes UTF-16, which breaks the dump. There, write the file inside the container with pg_dumpall -f and move it with docker cp.
Checklist for postgres:18
- Mount the volume at
/var/lib/postgresql, not/var/lib/postgresql/data. - If you set
PGDATAyourself, make sure it is inside the mounted volume. - Minor updates (18.5 → 18.6): just pull and recreate. Major upgrades: dump and restore (or
pg_upgrade), into a new volume. -
POSTGRES_PASSWORDand the other variables only apply when the data directory is empty; changing them later does not change the existing password.
The full chapter, with the Compose setup, a health check that doesn't report "healthy" too early, backups and the complete 16 → 18 upgrade output, is part of a free Docker tutorial: Run PostgreSQL in Docker.
Top comments (0)