Skip to content

Postgres in Docker: run, persist, compose and back up PostgreSQL in a container

Run PostgreSQL in Docker with docker run and Compose: the Postgres 18 data volume, init scripts, health checks, backups, upgrades and running it in production.

Abhishek Jaiswal

7 min

On this page 10 sections

To run PostgreSQL in Docker, start the official postgres image with a password and a named volume: docker run -d --name pg -e POSTGRES_PASSWORD=secret -p 5432:5432 -v pgdata:/var/lib/postgresql postgres:18. That gives you a Postgres 18 server on localhost:5432 whose data survives the container being removed. The one detail that changed recently is the volume path. From Postgres 18, the image keeps its data under /var/lib/postgresql. On 17 and older, mount /var/lib/postgresql/data.

This guide covers docker run, Docker Compose, init scripts, health checks, backups and major-version upgrades, then what a container leaves out once the data matters.

TL;DR

  • Use the official postgres image and pin the major version (postgres:18, not latest).
  • On Postgres 18 and newer, mount the volume at /var/lib/postgresql. On 17 and older, mount it at /var/lib/postgresql/data. The wrong path writes data to an anonymous volume you lose on re-create.
  • POSTGRES_PASSWORD is required. POSTGRES_USER and POSTGRES_DB are optional, and all three apply only on the first start with an empty volume.
  • In Compose, give Postgres a pg_isready health check and make the app wait for service_healthy.
  • A container is great for development. In production you also need scheduled backups, point-in-time recovery, failover and upgrades, which is the work a managed Postgres does.

How do I run Postgres in Docker?

Terminal window
docker run -d \
--name pg \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_USER=app \
-e POSTGRES_DB=app \
-p 5432:5432 \
-v pgdata:/var/lib/postgresql \
postgres:18

Then connect with any client:

Terminal window
psql "postgres://app:secret@localhost:5432/app"

Or open psql inside the container, which needs nothing installed on your machine:

Terminal window
docker exec -it pg psql -U app -d app

What each part does:

Flag Purpose
-e POSTGRES_PASSWORD Superuser password. Required, and it cannot be empty
-e POSTGRES_USER Creates this superuser and a database of the same name. Defaults to postgres
-e POSTGRES_DB Name of the default database. Defaults to the user name
-p 5432:5432 Publishes the port to your machine. Leave it out if only other containers connect
-v pgdata:/var/lib/postgresql A named volume, so data outlives the container

The environment variables take effect only when the data directory is empty. Changing POSTGRES_PASSWORD later does nothing to an existing database. Change it with ALTER USER instead.

Where does Postgres store data in Docker?

This is the most common way to lose data with the official image, and the answer depends on the major version.

Image Mount the volume at PGDATA inside it
postgres:18 and newer /var/lib/postgresql /var/lib/postgresql/18/docker
postgres:17 and older /var/lib/postgresql/data /var/lib/postgresql/data

The image changed the layout in 18 so that a major-version upgrade can use pg_upgrade --link across versions sitting side by side in one volume. If you copy a Compose file written for Postgres 16 and only bump the tag to 18, the old mount path no longer matches. Postgres then writes to an anonymous volume, and the data disappears the next time the container is re-created.

Check where your data actually lives:

Terminal window
docker exec pg psql -U app -c 'SHOW data_directory;'
docker inspect pg --format '{{ json .Mounts }}'

How do I run Postgres with Docker Compose?

A Compose file keeps the app, the database and the volume together:

services:
db:
image: postgres:18
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql
- ./initdb:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 10
ports:
- "5432:5432"
web:
build: .
environment:
DATABASE_URL: postgres://app:secret@db:5432/app
depends_on:
db:
condition: service_healthy
volumes:
pgdata:

Publish 5432 only if you want to reach the database from your machine. Other services in the file reach it either way.

The host is the service name

Inside the Compose network, the app connects to db:5432. localhost inside the web container is the web container itself.

Wait for the health check

depends_on alone only waits for the container to start. Postgres takes a few seconds to accept connections, so the health check with condition: service_healthy stops the app from crashing on its first connection attempt.

How do I run SQL when the container first starts?

Put .sql, .sql.gz or .sh files in a folder mounted at /docker-entrypoint-initdb.d, as in the Compose file above. The image runs them in name order after it creates the user and database:

-- initdb/01-extensions.sql
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS pgcrypto;

They run only on an empty data directory. If you add a script later, remove the volume (docker compose down -v, which deletes the data) or run the SQL by hand. If a script fails, the container exits, and the next start skips the scripts because the directory is no longer empty. Fix the script and recreate the volume.

The official image ships the contrib extensions such as pg_trgm, pgcrypto, citext and uuid-ossp. Extensions like pgvector and PostGIS need an image that includes them, such as pgvector/pgvector:pg18 or postgis/postgis.

How do I back up Postgres running in Docker?

Use pg_dump from inside the container and write the file to your machine:

Terminal window
docker exec pg pg_dump -U app -d app --format=custom > app.dump

Restore into a fresh database:

Terminal window
docker exec -i pg pg_restore -U app -d app --clean --if-exists < app.dump

Copying the volume’s files while Postgres is running does not give you a consistent backup. Stop the container first, or use pg_dump or pg_basebackup.

A nightly pg_dump from cron is a start. It still loses up to a day of writes, and it is only a backup once you have restored it somewhere and checked the data. Restoring to the minute needs WAL archiving with a tool such as pgBackRest or WAL-G.

How do I upgrade Postgres in Docker to a new major version?

Changing the tag from postgres:17 to postgres:18 on an existing volume does not upgrade the data. Postgres refuses to start on files from an older major version. The simplest path for small databases:

  1. Dump the old database: docker exec pg pg_dump -U app -d app --format=custom > app.dump.
  2. Start a new container on postgres:18 with a new, empty volume at the new path.
  3. Restore the dump into it with pg_restore, and point the app at the new container.

For large databases, pg_upgrade is faster, and the Postgres 18 volume layout exists to make it easier between future versions. Minor versions (18.5 to 18.6) need only a new image and a restart.

Should I run Postgres in Docker in production?

For development, CI and small internal tools, yes. A container gives everyone the same version with one command, and throwing the database away is a feature.

For data you cannot lose, the container is the easy part. The rest is the work around it:

You need In a container, you set up
Backups you can restore pg_dump on a schedule, off-machine storage, restore tests
Restore to a point in time WAL archiving with pgBackRest or WAL-G
A standby if the machine dies Replication, failover and a way to move clients over
Upgrades Minor-version patching, and a plan for each major version
Sizing Memory settings (shared_buffers, work_mem) matched to the host
Security Network rules, TLS, password rotation

If you are happy to own that list, Postgres in Docker on a VM you control works well. If not, a managed Postgres runs the same engine with that list handled.

How Ownkube runs Postgres

Ownkube runs managed PostgreSQL 17 and 18, so a database you develop against in postgres:18 locally runs on the same major version in production. Databases start at $4 a month for DB 0.5 (0.25 vCPU, 0.5 GiB), with storage at $0.25 per GB. On Personal and Team, that storage price includes 7-day backups with point-in-time restore. On Team, a database can add up to two standbys with automatic failover.

Extensions such as pgvector, PostGIS and pg_cron turn on from the database’s Settings tab, so there is no custom image to build. The database is private by default and reachable from your apps in the same environment.

To move a local database across, open a tunnel and restore the dump you already know how to make:

Terminal window
okctl connect my-database --tunnel-only --port 15432
pg_restore --no-owner -d "postgres://<user>:<password>@localhost:15432/<database>" app.dump

Keep docker compose up for local development. Your app container deploys from the same Dockerfile next to the managed database. Apps run in the Germany and Mumbai regions.

FAQ

What is the default Postgres username and password in Docker?

The default user is postgres, and there is no default password. POSTGRES_PASSWORD is required, so you set it the first time the container starts.

Why is my Postgres Docker data gone after a restart?

Usually the volume is mounted at the wrong path, so data went to an anonymous volume. On Postgres 18 and newer, mount at /var/lib/postgresql. On 17 and older, mount at /var/lib/postgresql/data. Data is also lost with docker compose down -v, which deletes named volumes.

Why does my app say “connection refused” to Postgres in Docker Compose?

Either the app connects to localhost instead of the service name (db), or it starts before Postgres accepts connections. Use the service name as the host and add a pg_isready health check with condition: service_healthy.

Why didn’t my POSTGRES_PASSWORD change take effect?

The environment variables apply only when the data directory is empty. On an existing volume, change the password with ALTER USER app WITH PASSWORD '...'.

Which Postgres Docker image should I use?

The official postgres image, pinned to a major version such as postgres:18. Use the default Debian-based tag unless image size matters, in which case postgres:18-alpine is smaller. For pgvector, use pgvector/pgvector:pg18.

Where Ownkube fits

Run Postgres in Docker for development, and hand production to a managed database when backups, restores and failover start to matter. On Ownkube that is Postgres 17 or 18 from $4 a month, with backups in the storage price on Personal and Team, next to your app on one bill. The add a Postgres database guide walks through it, and managed Postgres pricing compared prices the alternatives. Deploy my first app.

Abhishek Jaiswal · Founder and Engineer

Abhishek Jaiswal is the founder and engineer behind Ownkube, a cloud for personal software. He builds the platform and writes here about cloud cost, migrations, and running production infrastructure in your own AWS account without a DevOps team.

Share

More posts

All posts

Ship my first app in the next five minutes.

Push a repo and get a live URL, with no cloud account or config.

Top up from $5, billed per minute with no subscription.