Running PostgreSQL 18 with Docker Compose gives you a database for local development, CI, or a small single-server deployment without installing PostgreSQL on the host. In this guide you will run the official postgres:18.6 image with a pinned tag, credentials in an .env file, a named volume mounted at the new PostgreSQL 18 path, a healthcheck, init scripts, and tuned settings. You will then back the database up, restore it, and upgrade from an older major version.
Everything below was tested on Docker Engine 29 with PostgreSQL 18.6.
What changed in the PostgreSQL 18 image
If you copy a Compose file written for PostgreSQL 17 or older, it will fail to start on 18. Two things changed:
The data directory moved. From 18, the image stores data in
/var/lib/postgresql/18/docker(PGDATA), so you mount the volume at/var/lib/postgresql, not/var/lib/postgresql/data. A volume mounted at the old path makes the container exit with: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).The version-specific directory means a future 19 can sit next to 18 in the same volume during a
pg_upgrade.Data checksums are on by default for new clusters created by
initdb, so corruption on disk is detected instead of silently read.
Prerequisites
- Docker Engine with the Compose plugin (
docker compose). On Ubuntu see How to Install and Use Docker Engine on Ubuntu 26.04 LTS. - Port
5432free on the host, or pick another host port below.
docker version --format '{{.Server.Version}}'
docker compose version
Quick start with docker run
For a throwaway database, one command is enough:
docker run -d \
--name pg-test \
-p 127.0.0.1:5432:5432 \
-e POSTGRES_PASSWORD=change-me \
-v pg-test-data:/var/lib/postgresql \
postgres:18.6
Connect with docker exec -it pg-test psql -U postgres, and remove it with docker rm -f pg-test && docker volume rm pg-test-data. For anything you want to keep, use Compose.
Step 1: Create the project directory
mkdir -p ~/postgres/initdb && cd ~/postgres
Step 2: Store the credentials in an .env file
Create .env:
POSTGRES_USER=citizix_user
POSTGRES_PASSWORD=change-me-to-a-long-random-password
POSTGRES_DB=citizix_db
Lock it down and keep it out of Git:
chmod 600 .env
echo ".env" >> .gitignore
POSTGRES_USER: a superuser created at init. If unset, it ispostgres.POSTGRES_PASSWORD: required. The container refuses to start without it.POSTGRES_DB: a database created at init. If unset, it matchesPOSTGRES_USER.
These variables are only applied when the data directory is empty, on the first start. Changing them later does nothing. Change the password with SQL instead (Step 6).
Step 3: Add an init script (optional)
Any *.sql, *.sql.gz or *.sh file in /docker-entrypoint-initdb.d runs once, in alphabetical order, when the database is first created. Create initdb/01-app.sql:
CREATE TABLE IF NOT EXISTS notes (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
body text NOT NULL,
added timestamptz NOT NULL DEFAULT now()
);
INSERT INTO notes (body) VALUES ('hello from docker-entrypoint-initdb.d');
Like the .env values, init scripts do not run again on an existing volume.
Step 4: Create the Compose file
Create compose.yaml:
services:
postgres:
image: postgres:18.6
container_name: postgres
restart: unless-stopped
env_file: .env
ports:
- "127.0.0.1:5432:5432"
volumes:
- pg_data:/var/lib/postgresql
- ./initdb:/docker-entrypoint-initdb.d:ro
command:
- postgres
- -c
- shared_buffers=256MB
- -c
- max_connections=200
- -c
- log_min_duration_statement=500ms
shm_size: 256mb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
pg_data:
Key points:
- Pinned tag (
18.6):latestor18can change under you on the next pull. Minor updates (18.6 to 18.7) are safe to apply by bumping the tag. A major version change needs the upgrade in Step 9. pg_data:/var/lib/postgresql: the PostgreSQL 18 mount point (see above). The data ends up in18/dockerinside the volume.127.0.0.1:5432:5432: the database is reachable from the host only. Other containers in the same project connect topostgres:5432over the Compose network without any port mapping. Only bind to all interfaces if a firewall restricts who can reach it.commandwith-cflags: sets server parameters without maintaining a fullpostgresql.conf. Start withshared_buffersat about 25% of the memory you give the container.shm_size: Docker’s default/dev/shmis 64 MB, which parallel queries can exhaust.- Healthcheck:
pg_isreadypasses once the server accepts connections.$$stops Compose from expanding the variables itself, so the container’s environment is used. Application services can wait withdepends_on: { postgres: { condition: service_healthy } }.
Step 5: Start PostgreSQL
docker compose up -d --wait
Container postgres Healthy
Check the version, the data directory, the settings and the init script:
docker exec postgres psql -U citizix_user -d citizix_db -Atc "select version()" \
-c "show data_directory" -c "show shared_buffers" -c "show data_checksums" \
-c "select body from notes"
PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
/var/lib/postgresql/18/docker
256MB
on
hello from docker-entrypoint-initdb.d
Open an interactive shell with:
docker exec -it postgres psql -U citizix_user -d citizix_db
Or connect from the host with any client using:
postgresql://citizix_user:<password>@127.0.0.1:5432/citizix_db
Step 6: Manage users and databases
Give each application its own role and database instead of using the superuser:
docker exec postgres psql -U citizix_user -d postgres \
-c "CREATE ROLE app_user LOGIN PASSWORD 'another-long-random-password'" \
-c "CREATE DATABASE app_db OWNER app_user"
Change a password:
docker exec postgres psql -U citizix_user -d postgres \
-c "ALTER USER citizix_user PASSWORD 'new-long-random-password'"
Then update .env so it matches. For roles, grants and privileges in depth, see Postgres Create User, Create Database and Manage Access.
Step 7: Check persistence
Recreate the container and confirm the data survived:
docker compose down
docker compose up -d --wait
docker exec postgres psql -U citizix_user -d citizix_db -Atc "select count(*) from notes"
1
docker compose down keeps the volume. docker compose down -v deletes it, with all your data.
Step 8: Back up and restore
Logical backups with pg_dump
pg_dump in custom format (-Fc) is compressed and lets pg_restore pick objects. Dump from the running container to a file on the host:
docker exec postgres pg_dump -U citizix_user -d citizix_db -Fc > citizix_db-$(date +%F).dump
Roles and passwords are not in a per-database dump. Save them separately:
docker exec postgres pg_dumpall -U citizix_user --globals-only > globals-$(date +%F).sql
Restore into an empty database:
docker exec postgres psql -U citizix_user -d postgres -c "CREATE DATABASE citizix_db"
docker exec -i postgres pg_restore -U citizix_user -d citizix_db --no-owner < citizix_db-2026-11-10.dump
Note the -i on the restore: without it the dump is not passed to the container’s stdin.
Schedule the dump with cron and copy it off the host. For continuous archiving and point-in-time recovery, use pgBackRest instead. For more pg_dump options, see How to Backup and Restore a Postgres Database.
Step 9: Upgrade from PostgreSQL 17 or older
A major upgrade cannot reuse the old data directory: PostgreSQL 18 will not open files written by 17. The simplest path for a small or medium database is dump and restore into a new volume:
Dump everything from the old container, including roles:
docker exec postgres pg_dumpall -U citizix_user > all-$(date +%F).sql docker compose downChange the image to
postgres:18.6, the volume mount to/var/lib/postgresql, and give the volume a new name (for examplepg18_data), so the old data stays untouched as a fallback.Start 18 and load the dump:
docker compose up -d --wait docker exec -i postgres psql -U citizix_user -d postgres < all-2026-11-10.sqlErrors such as
role "citizix_user" already existsanddatabase "citizix_db" already existsare expected: the entrypoint already created them from.env.Check your data and the version, then remove the old volume once you are satisfied.
For large databases where a dump takes too long, use pg_upgrade, which needs the old and new binaries in one container. The version-specific 18/docker layout is designed for that.
Useful commands
docker compose logs -f postgres # follow the server log
docker exec postgres pg_isready -U citizix_user # is it accepting connections?
docker exec postgres psql -U citizix_user -d citizix_db -c '\l+' # databases and sizes
docker exec postgres psql -U citizix_user -d citizix_db -c 'select * from pg_stat_activity'
docker compose pull && docker compose up -d --wait # apply a minor update after bumping the tag
Related content
- How to Run pgAdmin 4 Using Docker and Docker Compose
- How to Run Django and Postgres in Docker Compose
- How to Install and Configure Postgres 17 on Rocky/Alma Linux 9
- pgBackRest: PostgreSQL Backup, Restore and PITR
- How to Run RabbitMQ 4 with Docker and Docker Compose
- Getting Started With Docker Compose With Examples
Conclusion
You now have PostgreSQL 18 running with Docker Compose: a pinned image, credentials outside the Compose file, the volume at the new PostgreSQL 18 path, init scripts, tuned settings, a healthcheck, and tested backup, restore and major-version upgrade steps. For production beyond a single server, add streaming replication or move to a managed or operator-run cluster.