Skip to main content

PostgreSQL with Docker Compose: A Working Setup (2026)

To run PostgreSQL with Docker Compose, you need the official postgres image, a password in POSTGRES_PASSWORD, and a named volume for the data. For PostgreSQL 18 and later, mount the volume at /var/lib/postgresql. Older versions use /var/lib/postgresql/data. Below is a complete file you can copy, then the details that usually go wrong.

Last updated: October 2026.

1. docker-compose.yml​

services:
db:
image: postgres:18
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pgdata:/var/lib/postgresql
ports:
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s

volumes:
pgdata:

Put the credentials in a .env file next to it. Compose reads it automatically:

POSTGRES_USER=app
POSTGRES_PASSWORD=change-me-to-something-long
POSTGRES_DB=app

Start it:

docker compose up -d
docker compose ps # wait for "healthy"

What each part does:

  • POSTGRES_PASSWORD is the only required variable. Without it the container exits on first start.
  • POSTGRES_USER and POSTGRES_DB are optional. They default to postgres.
  • The named volume keeps your data when the container is removed or recreated. See Docker Compose volumes.
  • 127.0.0.1:5432:5432 makes the database reachable from the host only. A plain "5432:5432" publishes it on every interface and bypasses ufw and firewalld. See Docker Compose ports.
  • The healthcheck lets other services wait until Postgres accepts connections. The $$ stops Compose from replacing the variables itself, so the shell inside the container reads them. See Docker Compose healthcheck.

2. The PostgreSQL 18 volume path​

The official image changed its data path in PostgreSQL 18. This is the most common reason a database "loses" its data after an upgrade or a copied tutorial.

Image versionPGDATAMount the volume at
postgres:17 and older/var/lib/postgresql/data/var/lib/postgresql/data
postgres:18 and newer/var/lib/postgresql/18/docker/var/lib/postgresql

If you copy an old tutorial and mount /var/lib/postgresql/data on postgres:18, the data is written outside your volume. It is gone the next time the container is recreated. The reverse also fails: on 17 and older, a volume at /var/lib/postgresql does not persist anything.

Check where your data actually lives:

docker compose exec db psql -U app -d app -c "SHOW data_directory;"

3. Connect to the database​

From another service in the same Compose file, use the service name as the host. The port is 5432. You do not need ports: for this, because services share a network. See Docker Compose networks.

services:
db:
# ... as above

web:
image: ghcr.io/example/web:latest
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
depends_on:
db:
condition: service_healthy

condition: service_healthy makes web wait for the healthcheck. A plain depends_on: [db] only waits for the container to start, and the app can crash on its first connection.

From the host, use 127.0.0.1:5432 with any client, or open psql inside the container:

docker compose exec db psql -U app -d app

4. Init scripts run only once​

Files in /docker-entrypoint-initdb.d/ (.sql, .sql.gz, .sh) run on the first start only, when the data directory is empty.

volumes:
- pgdata:/var/lib/postgresql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro

The same rule applies to POSTGRES_USER, POSTGRES_PASSWORD and POSTGRES_DB. If you change the password in .env after the first start, nothing happens. The old password stays in the database. To change it, run ALTER USER app PASSWORD '...'; in psql. Or, on a fresh database only, delete the volume with docker compose down -v and start again.


5. Back up and restore​

Back up with pg_dump through the running container:

docker compose exec -T db pg_dump -U app -d app > backup.sql

Restore into an empty database:

docker compose exec -T db psql -U app -d app < backup.sql

-T disables the TTY so the redirect works. Copy the file off the server. A backup on the same disk does not survive a disk failure. To copy the raw volume instead, see backing up a named volume. Stop the database first, or the copy can be inconsistent.


6. Upgrading PostgreSQL​

  • Minor versions (18.0 to 18.1) are safe. Pull and recreate:

    docker compose pull db
    docker compose up -d db
  • Major versions (17 to 18) are not. A data directory created by 17 does not start on 18. Dump the old database with pg_dump, start the new version on a new volume, and restore. pg_upgrade is the faster option for large databases, but it needs both versions installed.

Pin the major version in the tag (postgres:18, not postgres:latest). Then docker compose pull never upgrades you by surprise. For the general update flow, see updating images with Compose.


7. Common errors​

ErrorCauseFix
Database is uninitialized and superuser password is not specifiedPOSTGRES_PASSWORD missingSet it in .env
password authentication failed for userPassword changed in .env after first startALTER USER, or recreate the volume
connection refused from the appApp started before Postgres was readydepends_on with condition: service_healthy
could not translate host name "db"App is not in the same Compose project or networkPut both in one file, or share a network
could not resize shared memory segmentDefault /dev/shm is 64 MBAdd shm_size: 256mb to the db service
Data gone after docker compose upVolume mounted at the wrong path for the versionSee section 2

8. Deploy on Hostim.dev instead​

On a server, you own the backups, the upgrades and the firewall. On Hostim.dev, PostgreSQL is a managed service:

The database is backed up daily, and backups are kept for 7 days. You do not manage the volume path, the port or the firewall. See PostgreSQL on Hostim.dev.

Frequently asked questions

Where should I mount the Postgres volume in Docker Compose?

For postgres:18 and newer, mount the named volume at /var/lib/postgresql. For postgres:17 and older, mount it at /var/lib/postgresql/data. The image changed its data path in PostgreSQL 18, so a volume at the old path on a new image does not keep your data.

Why does my Postgres password change in .env not work?

POSTGRES_PASSWORD, POSTGRES_USER and POSTGRES_DB are only used when the data directory is empty, on the first start. After that, changing them has no effect. Change the password with ALTER USER in psql, or delete the volume with docker compose down -v if the database holds nothing you need.

How do I make my app wait for Postgres in Docker Compose?

Add a healthcheck to the Postgres service that runs pg_isready, then use depends_on with condition: service_healthy in the app service. A plain depends_on only waits for the container to start, not for Postgres to accept connections.

How do I connect to Postgres from another container in Docker Compose?

Use the service name as the host and port 5432, for example postgres://app:password@db:5432/app. Services in the same Compose project share a network, so you do not need to publish the port with ports: for this.

How do I back up a Postgres database running in Docker Compose?

Run docker compose exec -T db pg_dump -U app -d app > backup.sql. Restore with docker compose exec -T db psql -U app -d app < backup.sql. Store the backup file on a different machine than the database.