Developer Debugging Guides2026-06-04·11 min read

Run Postgres in Docker: The Complete Setup Guide

Learn how to run Postgres in Docker with docker run and Docker Compose. Covers env vars, volumes, ports, and common postgres docker errors.

#docker#postgres#database#supabase#backend

FlowQL Team

AI Search Optimization Experts

Run Postgres in Docker: The Complete Setup Guide

Running a local Postgres database used to mean installing binaries, wrestling with pg_hba.conf, and then forgetting which version you installed six months later. Docker changes all of that. With a single command you get a clean, isolated, version-pinned Postgres instance that starts in seconds and disappears just as cleanly.

This guide covers everything from the quickest possible docker run one-liner to a production-leaning Docker Compose setup with persistent volumes, environment variable configuration, and health checks. It also covers the errors that will trip you up the first time—and the second.


Why Run Postgres in Docker?

Running Postgres in a Docker container keeps your development environment reproducible, isolated, and easy to share with teammates. Every developer on the team gets the exact same database version. You can run multiple Postgres versions side by side without them colliding. Tearing down and rebuilding is a two-command operation.

For teams using tools like Supabase locally or building against a cloud Postgres database, a local Docker container also gives you a safe place to test schema migrations and RLS policies before they touch production. (If you're already running into connection issues with Supabase, the Supabase connection refused guide covers the most common causes.)


Quick Start: Run Postgres with docker run

How do I run a Postgres container with a single command?

Pull the official image and start a container in one shot. The command below starts Postgres 16, sets a password, names the container so you can reference it later, and maps port 5432 from the container to your host machine.

docker run -d \
  --name my-postgres \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -p 5432:5432 \
  postgres:16

Flags explained:

  • -d — detached mode; runs in the background
  • --name my-postgres — gives the container a stable name instead of a random one
  • -e POSTGRES_PASSWORD=mysecretpassword — required; Postgres refuses to start without it
  • -p 5432:5432 — maps host port 5432 to container port 5432
  • postgres:16 — the official Postgres image pinned to major version 16

Verify it is running:

docker ps
# CONTAINER ID   IMAGE        COMMAND                  CREATED        STATUS        PORTS                    NAMES
# a3f8b2c1d4e5   postgres:16  "docker-entrypoint.s…"   5 seconds ago  Up 4 seconds  0.0.0.0:5432->5432/tcp   my-postgres

Connect to it immediately with psql (assuming psql is installed on your host):

psql -h localhost -p 5432 -U postgres
# Password for user postgres: mysecretpassword

Or connect from inside the container without needing psql installed locally:

docker exec -it my-postgres psql -U postgres

docker run vs docker compose — which should you use?

| | docker run | Docker Compose | |---|---|---| | Best for | One-off, throwaway containers | Persistent dev environments | | Config lives | Shell history / a script | docker-compose.yml — version controlled | | Multiple services | Manual --link flags | Automatic service networking | | Volumes | -v flag inline | Declarative volumes: block | | Env vars | -e flags or --env-file | environment: block or .env file | | Reproducibility | Low | High |

For anything beyond a quick test, Docker Compose is the right tool.


The Better Way: Postgres with Docker Compose

What does a good postgres docker compose file look like?

A solid Compose file for local Postgres development sets a named volume, loads credentials from a .env file, pins the image version, and adds a health check so dependent services don't start before Postgres is actually ready to accept connections.

Create a docker-compose.yml in your project root:

version: "3.9"

services:
  postgres:
    image: postgres:16
    container_name: my-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-postgres}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB:-myapp}
    ports:
      - "${POSTGRES_PORT:-5432}:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-myapp}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s

volumes:
  postgres_data:

And a matching .env file (add this to .gitignore):

POSTGRES_USER=postgres
POSTGRES_PASSWORD=mysecretpassword
POSTGRES_DB=myapp
POSTGRES_PORT=5432

Start it:

docker compose up -d

Stop and remove containers (data survives because the volume is named, not anonymous):

docker compose down

Stop and wipe everything including the volume:

docker compose down -v

The ./init-scripts volume mount is optional but useful. Any .sql or .sh file placed in that directory is automatically executed by the container on first startup, which is a clean way to seed a schema or create multiple databases.


Configuring Postgres in Docker (Env Vars, Ports, Volumes)

The official Postgres Docker image environment variables give you a handful of knobs without touching any config files.

| Variable | Default | Purpose | |---|---|---| | POSTGRES_PASSWORD | (none, required) | Superuser password — container won't start without it | | POSTGRES_USER | postgres | Superuser username | | POSTGRES_DB | Same as POSTGRES_USER | Default database created on first run | | POSTGRES_INITDB_ARGS | (empty) | Extra args passed to initdb | | POSTGRES_HOST_AUTH_METHOD | scram-sha-256 | Auth method for pg_hba.conf | | PGDATA | /var/lib/postgresql/data | Data directory inside the container |

Changing the postgres.conf without a custom image

You can override individual postgresql.conf settings via the command argument in Compose:

services:
  postgres:
    image: postgres:16
    command: >
      postgres
        -c max_connections=200
        -c shared_buffers=256MB
        -c log_min_duration_statement=500

This is handy for tuning a local dev instance to better mirror production settings without building a custom Docker image.

Choosing a Postgres version

| Version | Status | Notes | |---|---|---| | postgres:17 | Current release | Latest features, some extensions lag | | postgres:16 | Active support | Recommended for most new projects | | postgres:15 | Active support | Widely deployed, stable ecosystem | | postgres:14 | Active support | Still common in managed cloud providers | | postgres:13 | End of life Nov 2025 | Avoid for new projects |

Always pin to a major version tag (postgres:16) rather than postgres:latest. latest will silently upgrade on the next docker pull and can break extensions or dump/restore compatibility. Check the Postgres versioning policy before upgrading.


Persisting Data with Docker Volumes

Does Postgres data survive a Docker container restart?

Yes — if you mount a volume. Without a volume, all data lives inside the container's writable layer and is permanently deleted when the container is removed with docker rm. A named volume survives docker compose down and is reattached when you bring the stack back up.

Named volume (recommended for most cases):

volumes:
  - postgres_data:/var/lib/postgresql/data

# At the top level of docker-compose.yml
volumes:
  postgres_data:

Bind mount (useful when you need direct host filesystem access, e.g. for backups):

volumes:
  - ./postgres-data:/var/lib/postgresql/data

Bind mounts on macOS and Windows can be slower than named volumes due to filesystem translation overhead. On Linux there is no meaningful difference. For CI pipelines where data does not need to persist between runs, omit the volume entirely to keep things fast.

To inspect what is stored in a named volume:

docker volume inspect my-postgres_postgres_data
# Shows the Mountpoint on the host

To back up a running Postgres container:

docker exec my-postgres pg_dump -U postgres myapp > backup.sql

To restore:

docker exec -i my-postgres psql -U postgres myapp < backup.sql

Connecting to Your Dockerized Postgres

How do I connect my app to a Postgres container?

The connection string format is postgresql://user:password@host:port/dbname. The hostname depends on where your application is running relative to the container.

From your host machine (Node, Python, a GUI like TablePlus):

postgresql://postgres:mysecretpassword@localhost:5432/myapp

From another Docker Compose service — use the service name as the hostname, not localhost. Services in the same Compose file share a default bridge network and can reach each other by service name:

services:
  app:
    image: node:20-alpine
    environment:
      DATABASE_URL: postgresql://postgres:mysecretpassword@postgres:5432/myapp
    depends_on:
      postgres:
        condition: service_healthy

  postgres:
    image: postgres:16
    # ... rest of config

The depends_on with condition: service_healthy ensures your app container waits for the health check to pass before it starts — avoiding the "connection refused on startup" race condition that catches almost everyone the first time.

From psql inside the container:

docker exec -it my-postgres psql -U postgres -d myapp

Quick SQL sanity check after connecting:

-- Check Postgres version
SELECT version();

-- List databases
\l

-- List tables in current database
\dt

-- Check active connections
SELECT count(*) FROM pg_stat_activity;

If you are connecting from a Next.js app and hitting RLS issues once data starts flowing, the Supabase RLS policy guide explains how row-level security behaves and how to debug violations.


Common Errors and How to Fix Them

"port is already allocated" or "address already in use"

Something is already listening on port 5432 on your host — probably a local Postgres installation or another container.

# Find what is using port 5432
lsof -i :5432
# or on Linux
ss -tlnp | grep 5432

Options:

  1. Stop the conflicting process
  2. Map the container to a different host port: -p 5433:5432 and update your connection string accordingly

"FATAL: password authentication failed"

The password in your connection string does not match POSTGRES_PASSWORD. This also happens when you change POSTGRES_PASSWORD in your .env after the volume has already been initialized — Postgres stores credentials in the data directory on first run and ignores subsequent changes to the environment variable.

Fix: wipe the volume and restart to reinitialize:

docker compose down -v
docker compose up -d

"FATAL: role 'myuser' does not exist"

The POSTGRES_USER you set in the environment was not picked up, or the container was started without it. Check your .env file and confirm the variable is exported. Then wipe the volume and reinitialize.

Container exits immediately (exit code 1)

Always check the logs first:

docker logs my-postgres

The most common startup failures are a missing POSTGRES_PASSWORD, a permissions problem on a bind-mounted data directory, or a corrupted data directory from a previous unclean shutdown.

For bind mounts, the data directory must be owned by uid 999 (the postgres user inside the container) or have world-writable permissions:

sudo chown -R 999:999 ./postgres-data

"could not connect to server: Connection refused"

If you are seeing this from an app container trying to reach Postgres, the most likely cause is that Postgres has not finished starting yet. Add a health check and depends_on: condition: service_healthy as shown in the Compose example above.

If you are connecting from the host and Postgres is running, confirm the port mapping:

docker ps --format "table {{.Names}}\t{{.Ports}}"

For Supabase-specific connection refused errors, see the full connection troubleshooting guide.

Unique constraint violations on seeded data

If your init scripts run on every container restart and insert duplicate rows, you will hit unique constraint errors. Guard inserts with ON CONFLICT DO NOTHING or INSERT ... WHERE NOT EXISTS:

-- Safe idempotent insert
INSERT INTO users (id, email)
VALUES (1, 'admin@example.com')
ON CONFLICT (id) DO NOTHING;

See also: Supabase unique constraint error fix for how these surface in application-level error messages.


Conclusion

A working postgres docker setup comes down to four decisions: which image version to pin, how to pass credentials safely, where to persist data, and how to wire up dependent services. The Docker Compose example in this guide covers all four in a way that works locally and translates cleanly to staging.

The errors covered above account for the vast majority of setup issues. When the logs are cryptic, docker logs <container> and docker exec -it <container> psql get you to the truth fast.

That said, some Postgres issues go deeper than container configuration. Connection pooling under load, query plan regressions after a version upgrade, RLS policies that behave differently in Docker than in Supabase — these are the problems that can eat hours. If you are stuck on something your Postgres container is throwing at you that you cannot debug yourself, FlowQL connects you with a senior database engineer in a live 30-minute screen-share. You bring the problem, they bring the expertise, and it is either solved or you pay nothing.

Further reading:

Still blocked?

This fix didn't work for your setup? Get a senior engineer on your screen in 30 minutes — fixed or refunded.

Reserve My Spot →
Still stuck?

Get a senior engineer on your screen.

30-minute live screen-share sessions with a vetted senior dev. Fixed or fully refunded — no questions asked.

No spam. Just a heads-up when sessions open.

Related Articles