Installing Postgres directly on your machine works, but it gets messy fast once you’re juggling multiple projects that each want different versions, extensions, or seed data. Docker sidesteps all of that you get a clean, disposable Postgres instance per project, and your host machine stays untouched.
This guide covers running Postgres in Docker for local development: quick one-off containers, docker-compose for anything you’ll come back to, persistent data, and a few things that trip people up.
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
Enter fullscreen mode Exit fullscreen mode
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 topostgresif omitted -
-p 5432:5432— maps container port 5432 to host port 5432 -
-d— detached, runs in the background -
postgres:16— pin a version; avoidlatestsince it can silently jump major versions later
Check it’s running:
docker ps
Enter fullscreen mode Exit fullscreen mode
Connect with psql (if installed locally) or from inside the container:
docker exec -it my-postgres psql -U devuser -d myapp
Enter fullscreen mode Exit fullscreen mode
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:
Enter fullscreen mode Exit fullscreen mode
Start it:
docker compose up -d
Enter fullscreen mode Exit fullscreen mode
Stop it (keeps data):
docker compose down
Enter fullscreen mode Exit fullscreen mode
Stop and wipe data:
docker compose down -v
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
If you’d rather control the exact host path :
volumes:
- ./pgdata:/var/lib/postgresql/data
Enter fullscreen mode Exit fullscreen mode
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:
Enter fullscreen mode Exit fullscreen mode
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:
Enter fullscreen mode Exit fullscreen mode
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_USERon 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
Enter fullscreen mode Exit fullscreen mode
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.
답글 남기기