How to Run PostgreSQL 18 with Docker and Docker Compose

Run PostgreSQL 18 with Docker Compose: pinned image, the new PG18 volume path, .env credentials, healthcheck, init scripts, tuning, backups and upgrades.

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 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 is postgres.
  • POSTGRES_PASSWORD: required. The container refuses to start without it.
  • POSTGRES_DB: a database created at init. If unset, it matches POSTGRES_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): latest or 18 can 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 in 18/docker inside the volume.
  • 127.0.0.1:5432:5432: the database is reachable from the host only. Other containers in the same project connect to postgres:5432 over the Compose network without any port mapping. Only bind to all interfaces if a firewall restricts who can reach it.
  • command with -c flags: sets server parameters without maintaining a full postgresql.conf. Start with shared_buffers at about 25% of the memory you give the container.
  • shm_size: Docker’s default /dev/shm is 64 MB, which parallel queries can exhaust.
  • Healthcheck: pg_isready passes once the server accepts connections. $$ stops Compose from expanding the variables itself, so the container’s environment is used. Application services can wait with depends_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:

  1. Dump everything from the old container, including roles:

    docker exec postgres pg_dumpall -U citizix_user > all-$(date +%F).sql
    docker compose down
    
  2. Change the image to postgres:18.6, the volume mount to /var/lib/postgresql, and give the volume a new name (for example pg18_data), so the old data stays untouched as a fallback.

  3. 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.sql
    

    Errors such as role "citizix_user" already exists and database "citizix_db" already exists are expected: the entrypoint already created them from .env.

  4. 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

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.

comments powered by Disqus
Citizix Ltd
Built with Hugo
Theme Stack designed by Jimmy