Running PostgreSQL with Docker
DEV Community

Running PostgreSQL with Docker

1. The quickest way to get a Postgres instance running

docker run --name my-postgres \
  -e POSTGRES_PASSWORD=secret \
  -e POSTGRES_USER=devuser \
  -e POSTGRES_DB=myapp \
  -p 5432:5432 \
  -d postgres:16

Breaking that down:

  • --name my-postgres - a friendly name so you can reference the container later instead of a random hash.
  • POSTGRES_PASSWORD - required; the container won't start without it.
  • POSTGRES_USER / POSTGRES_DB - optional; default to postgres if omitted.
  • -p 5432:5432 - maps container port 5432 to host port 5432.
  • -d - detached, runs in the background.
  • postgres:16 - pin a version; avoid latest since it can silently jump major versions later.

Check it's running:

docker ps

Connect with psql (if installed locally) or from inside the container:

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

2. Using docker-compose for anything persistent

For a real project, docker-compose.yml is the better default - it's version-controlled, reproducible, and easy to extend with more services later (Redis, pgAdmin, your app itself).

services:
  db:
    image: postgres:16
    container_name: myapp-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Start it:

docker compose up -d

Stop it (keeps data):

docker compose down

Stop and wipe data:

docker compose down -v

3. Why the volume matters

Without a named volume, all data lives inside the container's writable layer - delete the container, lose the database. The pgdata:/var/lib/postgresql/data mapping above stores Postgres's actual data files in a Docker-managed volume that survives container restarts and even docker compose down (without -v).

To see where Docker keeps it on disk:

docker volume inspect myapp_pgdata

If you'd rather control the exact host path:

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

4. Seeding initial data

Postgres's official image runs any .sql or .sh files placed in /docker-entrypoint-initdb.d/, but only on first startup, when the data directory is empty.

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init:/docker-entrypoint-initdb.d

volumes:
  pgdata:

Drop a file at ./init/001-schema.sql with your CREATE TABLE statements, and it runs automatically the first time the container spins up with a fresh volume. This is a solid pattern for local dev seed data, though it won't run again once the volume already has data - if you need to reseed, docker compose down -v first.

5. Health checks

If you're running your Go/Node/whatever app as another service in the same compose file, a health check stops it from starting before Postgres is actually ready to accept connections:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: myapp
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U devuser -d myapp"]
      interval: 5s
      timeout: 5s
      retries: 5

  app:
    build: .
    depends_on:
      db:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://devuser:secret@db:5432/myapp

volumes:
  pgdata:

Note the connection string from app uses db as the host, not localhost - inside Docker's network, services reach each other by container/service name.

6. Common gotchas

  • "password authentication failed" after changing env vars. Postgres only applies POSTGRES_PASSWORD / POSTGRES_USER on first initialization of an empty data directory. If you change them later, the existing volume still has the old credentials. You'll need to either update the password inside Postgres directly (ALTER USER devuser WITH PASSWORD 'newpass';) or wipe the volume and start fresh.

  • Port already in use. If Postgres is also installed locally, port 5432 will conflict. Either stop the local service or map to a different host port: -p 5433:5432.

  • Data "disappearing" between runs. Almost always means no volume was mounted, or a different volume name was used across runs. Double-check docker volume ls.

  • Connecting from your host app vs. a containerized app. From your host machine, use localhost:5432. From another container in the same compose file, use the service name (db:5432).

7. Quick reference

# Start
docker compose up -d

# View logs
docker compose logs -f db

# Open a psql shell
docker exec -it myapp-postgres psql -U devuser -d myapp

# Stop (keep data)
docker compose down

# Stop and delete data
docker compose down -v

# Back up a database
docker exec myapp-postgres pg_dump -U devuser myapp > backup.sql

# Restore
cat backup.sql | docker exec -i myapp-postgres psql -U devuser -d myapp

For local development, docker-compose with a named volume covers almost everything you need: a clean, disposable Postgres instance, seed data on first run, and health checks so dependent services don't race ahead of the database. It's a small setup cost that saves you from "works on my machine" version mismatches down the line.

Comments

No comments yet. Start the discussion.