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_PASSWORDis the only required variable. Without it the container exits on first start.POSTGRES_USERandPOSTGRES_DBare optional. They default topostgres.- The named volume keeps your data when the container is removed or recreated. See Docker Compose volumes.
127.0.0.1:5432:5432makes the database reachable from the host only. A plain"5432:5432"publishes it on every interface and bypassesufwandfirewalld. 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 version | PGDATA | Mount 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 dbdocker 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_upgradeis 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
| Error | Cause | Fix |
|---|---|---|
Database is uninitialized and superuser password is not specified | POSTGRES_PASSWORD missing | Set it in .env |
password authentication failed for user | Password changed in .env after first start | ALTER USER, or recreate the volume |
connection refused from the app | App started before Postgres was ready | depends_on with condition: service_healthy |
could not translate host name "db" | App is not in the same Compose project or network | Put both in one file, or share a network |
could not resize shared memory segment | Default /dev/shm is 64 MB | Add shm_size: 256mb to the db service |
Data gone after docker compose up | Volume mounted at the wrong path for the version | See 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:
- Create a project and choose Paste Docker Compose
- Paste the file from section 3. The
dbservice becomes a managed PostgreSQL database, andDATABASE_URLinwebis rewritten to point at it. Check the env vars before you deploy.
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.