In Docker Compose, Postgres 18 keeps its data in a volume mounted at /var/lib/postgresql: the line pgdata:/var/lib/postgresql under the service’s volumes. Postgres 17 and older use /var/lib/postgresql/data. The one variable the image requires is POSTGRES_PASSWORD.
The docker run command, converted
This is the storage command from the image’s Docker Hub page, with the tag pinned, a named volume in place of the host folder, and the --shm-size its caveats suggest:
docker run --name some-postgres --shm-size=256MB -v pgdata:/var/lib/postgresql -e POSTGRES_PASSWORD=mysecretpassword -d postgres:18
services:
some-postgres:
image: postgres:18
volumes:
- pgdata:/var/lib/postgresql
environment:
POSTGRES_PASSWORD: mysecretpassword
shm_size: 256MB
volumes:
pgdata:
name: pgdata
name: pgdata makes Compose use the volume docker run created, data and all. --shm-size becomes shm_size, written as given: the default 64 MB of /dev/shm can fail large queries with could not resize shared memory segment. -d needs no key.
Postgres 18 moved the data directory
From 18 on, the image sets PGDATA to /var/lib/postgresql/18/docker and declares its volume one level up, so a later pg_upgrade --link sees both versions in one mount. A file carried over from 17 with /var/lib/postgresql/data doesn’t start: the container exits with Error: in 18+, these Docker images are configured to store database data in a format which is compatible with "pg_ctlcluster". On 17 and older the opposite mistake is silent: a mount at /var/lib/postgresql sits above the data, which goes to an anonymous volume and is gone when the container is recreated.
A database the app waits for
services:
db:
image: postgres:18
environment:
POSTGRES_PASSWORD: mysecretpassword
volumes:
- pgdata:/var/lib/postgresql
ports:
- "127.0.0.1:5432:5432"
shm_size: 256MB
healthcheck:
test: ["CMD-SHELL", "pg_isready -h localhost -U postgres"]
interval: 5s
timeout: 5s
retries: 10
adminer:
image: adminer:6
ports:
- "127.0.0.1:8080:8080"
depends_on:
db:
condition: service_healthy
volumes:
pgdata:
-h localhost makes pg_isready use TCP. While the entrypoint runs initdb and the scripts in /docker-entrypoint-initdb.d, a temporary server listens on the Unix socket only, so a plain pg_isready already says accepting connections and the app connects to a server that is about to restart.
Gotchas
POSTGRES_PASSWORD,POSTGRES_USER,POSTGRES_DBand the init scripts only act on an empty data directory. Changing them later does nothing; useALTER USERinstead.docker compose down -vdeletes the volumes Compose created, database included. Aname:volume thatdocker runmade first is left in place."5432:5432"listens on every interface, and on Linux Docker’s iptables rules let it past ufw.127.0.0.1:keeps it local; other services reachdb:5432without anyports.postgres:18follows minor releases (18.6 today) andpostgres:18.6pins one.latestmoves to 19 when it ships.
GitHub Actions
A service container takes docker create flags under options, and the job’s steps start once it’s healthy:
services:
postgres:
image: postgres:18
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -h localhost -U postgres"
--health-interval 5s
--health-retries 10