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 KarunaratneIshan Karunaratne⏱️ 7 min readUpdated
Share thisCopied

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 container
docker stop :container_name && docker rm :container_name

# Start a new Postgres container with the same volume — your data is intact
docker 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 postgres
# password 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_version

# From 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

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts