TechEarl

How to Run PostgreSQL in Docker (With Persistent Storage)

Run a Postgres server in a container, give it a named volume so your data survives, set the password the right way, run pg_isready as a healthcheck, and connect from the host or another container.

Ishan Karunaratne⏱️ 8 min readUpdated
Share thisCopied
Run Postgres 16 or 17 in Docker with a named volume so your data survives container removal. Set the password, connect with psql, use pg_isready for healthchecks, and back up cleanly.

PostgreSQL in Docker is one command and a volume. The official postgres image handles initialization, user/database creation, and exposes the right port; you just need to set the password env var and mount a named volume on the data directory so the container can come and go without taking your data with it.

How do I run PostgreSQL in Docker?

bash
docker run -d --name postgres \
  -e POSTGRES_PASSWORD=change-me \
  -p 5432:5432 \
  -v pgdata:/var/lib/postgresql/data \
  postgres:17

Postgres 17 in the background, password set, port 5432 published, named volume pgdata on the data directory so you can docker rm the container without losing tables.

Try it with your own values

Configure the version, ports, password, and volume.

Pick a version

  • postgres:17 — Postgres 17, current major (released September 2024). Recommended for new projects.
  • postgres:16 — Postgres 16, still supported, very common in production.
  • postgres:15, postgres:14, postgres:13 — older majors, all still receiving security fixes through their end-of-life dates (13 EOL November 2025, 14 EOL November 2026, 15 EOL November 2027, 16 EOL November 2028).
  • postgres:17-alpine — Alpine variant, smaller but musl libc.

Postgres has a fixed 5-year support lifecycle per major. Pin to the major (postgres:17); the patch level auto-updates on docker pull.

The basic run command

bash
docker run -d --name :container_name 
-e POSTGRES_PASSWORD=:password 
-p :host_port:5432 
-v :volume:/var/lib/postgresql/data 
postgres::pg_version

The flags:

  • POSTGRES_PASSWORD — required; the image refuses to start without it (unless you use the trust host-auth-method, which is the next thing).
  • -p :host_port:5432 — publishes port 5432 on the host.
  • -v :volume:/var/lib/postgresql/data — the data directory volume.

Optional init env vars (only honored on first run, empty data directory):

bash
docker run -d --name :container_name 
-e POSTGRES_PASSWORD=:password 
-e POSTGRES_USER=appuser 
-e POSTGRES_DB=myapp 
-p :host_port:5432 
-v :volume:/var/lib/postgresql/data 
postgres::pg_version
  • POSTGRES_USER creates a superuser with that name (instead of the default postgres). The image then uses this user for any POSTGRES_DB it creates.
  • POSTGRES_DB creates an initial database. Default is the same name as the user.

For local testing without a password, use:

bash
docker run -d --name :container_name 
-e POSTGRES_HOST_AUTH_METHOD=trust 
-p :host_port:5432 
-v :volume:/var/lib/postgresql/data 
postgres::pg_version

trust means "no password needed for any connection." Do this only on a local dev machine; do not expose port 5432 to a network.

Why you need a volume

Postgres stores everything (tables, indexes, WAL, configuration files) under /var/lib/postgresql/data inside the container. Without a volume mounted there, the data lives in the container's writable layer and is destroyed by docker rm. With -v pgdata:/var/lib/postgresql/data, Docker manages a persistent volume that survives container removal.

bash
Stop and remove the containerdocker stop :container_name && docker rm :container_nameStart a new Postgres container with the same volume — your data is intactdocker run -d --name :container_name 
-e POSTGRES_PASSWORD=:password 
-p :host_port:5432 
-v :volume:/var/lib/postgresql/data 
postgres::pg_version

Important: the data directory format changes between major Postgres versions. A volume initialized by Postgres 16 cannot be mounted by Postgres 17 directly — Postgres 17 will refuse to start with a "incompatible data directory" error. To upgrade across majors, use pg_dump from the old version, start a new container with the new version on a fresh volume, and pg_restore. Or use pg_upgrade in a more involved migration. Pinning to a specific major (postgres:17 not postgres:latest) is what keeps this from biting you on a docker pull.

Bind mount alternative:

bash
mkdir -p ~/docker-data/pg
docker run -d --name :container_name 
-e POSTGRES_PASSWORD=:password 
-p :host_port:5432 
-v ~/docker-data/pg:/var/lib/postgresql/data 
postgres::pg_version

Bind mounts to host paths sometimes hit permission issues on Mac and Windows (Docker Desktop's virtual filesystem). For databases, named volumes are easier.

Practical usage: connecting

With psql from the container:

bash
docker exec -it :container_name psql -U postgres

-U postgres matches the default user. If you set POSTGRES_USER=appuser, use that name.

From the host with psql:

bash
psql -h 127.0.0.1 -p :host_port -U postgrespassword prompt

Connection string for an app:

code
postgres://postgres::password@127.0.0.1::host_port/postgres

postgres at the end is the default database name. Replace with POSTGRES_DB value if you set one.

From another container on the same Docker network:

bash
docker network create app-net
docker run -d --name :container_name --network app-net 
-e POSTGRES_PASSWORD=:password 
-v :volume:/var/lib/postgresql/data 
postgres::pg_versionFrom another container:docker run -it --rm --network app-net postgres::pg_version 
psql -h :container_name -U postgres

The Postgres container is reachable as :container_name on port 5432 from anything else on app-net; no port publishing needed for in-network traffic.

Healthcheck with pg_isready

The image ships pg_isready for readiness probing — useful in Compose to wait for the database before starting dependent services:

yaml
services:
db:
image: postgres::pg_version
environment:
POSTGRES_PASSWORD: :password
volumes:
:volume:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
web:
image: my-app
depends_on:
db:
condition: service_healthy

Without condition: service_healthy, web starts before the database is actually accepting connections and fails. See Docker Restart Policies and Health Checks.

Backups

Logical backup with pg_dump:

bash
docker exec :container_name 
pg_dump -U postgres -Fc postgres > backup-$(date +%Y%m%d).dump

-Fc is the custom format — compressed and works with pg_restore. Restore:

bash
docker exec -i :container_name 
pg_restore -U postgres -d postgres --clean --if-exists < backup-$(date +%Y%m%d).dump

Logical backup of all databases:

bash
docker exec :container_name 
pg_dumpall -U postgres > all-dbs-$(date +%Y%m%d).sql

Volume snapshot (requires stopping for consistency):

bash
docker stop :container_name
docker run --rm -v :volume:/source:ro -v "$(pwd):/backup" alpine 
tar czf /backup/pg-data-$(date +%Y%m%d).tar.gz -C /source .
docker start :container_name

Docker Compose version

yaml
services:
db:
image: postgres::pg_version
environment:
POSTGRES_PASSWORD: :password
POSTGRES_USER: appuser
POSTGRES_DB: myapp
ports:
"::host_port:5432"
volumes:
:volume:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
volumes:
:volume:

Common pitfalls

  • POSTGRES_PASSWORD not set — image refuses to start. Either set it or set POSTGRES_HOST_AUTH_METHOD=trust for local-only use.
  • Init env vars (POSTGRES_USER, POSTGRES_DB) ignored on second run. Only honored on first init of an empty data directory. If the volume already has data, create users/databases with SQL.
  • Major version bump breaking the volume. Going from postgres:16 to postgres:17 on the same volume fails — Postgres won't read a data directory from a previous major. Always pin to a major, and migrate explicitly with pg_dump / pg_restore or pg_upgrade.
  • Locale errors on Alpine. postgres:17-alpine is smaller but the locale data is minimal. If your app needs specific locales (Turkish text sort, etc.), use the regular Debian-based image.
  • Connecting from the host with localhost. Use 127.0.0.1. Some clients interpret localhost as the Unix socket, which doesn't exist on the host for a containerized server.
  • Port 5432 already in use — you have another Postgres on the host. Use a different host port.

What to do next

FAQ

Sources

Authoritative references this article was fact-checked against.

TagsDockerPostgreSQLPostgresDatabasePersistenceDevOps

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts