
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Installing PostgreSQL directly on your host machine creates version conflicts, makes cleanup difficult, and rarely matches your production environment. When you run PostgreSQL in Docker for development, you get an isolated, reproducible database that mirrors production behavior without polluting your OS. This guide covers the exact Docker Compose configuration I use for client projects, including persistent storage, health checks, and initialization scripts that prevent the most common data-loss mistakes.
How do you configure Docker Compose to run PostgreSQL in Docker for development?
The foundation of a reliable local database is a correct docker-compose.yml. Many tutorials omit critical settings like health checks or use bind mounts that fail silently on permission errors. Before writing any compose file, review my Docker Compose multi-container setup guide for foundational patterns that apply beyond databases.
Minimal but production-aligned compose file
services:
postgres:
image: postgres:16-alpine
container_name: pg-dev
restart: unless-stopped
environment:
POSTGRES_DB: app_dev
POSTGRES_USER: devuser
POSTGRES_PASSWORD: devpass_change_me
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d app_dev"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
volumes:
pgdata: This configuration addresses the three failures I see most often in code reviews:
- Named volume instead of bind mount: Bind mounts (
./data:/var/lib/postgresql/data) frequently fail on Linux due to UID/GID mismatches between the host and the postgres user inside the container. Named volumes are managed by Docker and avoid this entirely. - Explicit health check: Without
pg_isready, dependent services (your app, migration runner) will attempt connections before PostgreSQL finishes recovery after a crash or restart, causing confusing startup failures. - Alpine variant: The
-alpinetag reduces image size from ~430MB to ~80MB, speeding up pulls and CI cache hydration without sacrificing functionality for development.
Why environment variables over .env files for passwords
For local development, inline environment variables are acceptable and more visible during debugging. However, never commit real credentials. Add docker-compose.override.yml to your .gitignore and override sensitive values there, or use Docker secrets if your team requires it even locally. In production-adjacent staging environments, I always reference Kubernetes secrets management patterns or HashiCorp Vault rather than plaintext env vars.
How do you persist PostgreSQL data across container restarts?
Data persistence is where most developers lose work. Understanding the difference between ephemeral container filesystems and durable volumes prevents catastrophic resets during routine maintenance.
Verifying volume attachment
After starting your stack, confirm the volume is correctly mounted:
docker exec pg-dev df -h /var/lib/postgresql/data
# Expected output shows /dev/sdX or overlay, NOT tmpfs
docker volume inspect pgdata
# Check "Mountpoint" field exists and is accessible If df shows tmpfs or the mountpoint is missing, your volume declaration has a typo or indentation error. Fix it immediately — every write since the last successful mount exists only in the container's writable layer and will vanish on next down.
Safe volume reset procedure
Sometimes you need a clean slate (schema changes, corrupted state). Do this safely:
- Stop the container:
docker compose stop postgres - Create a backup:
docker run --rm -v pgdata:/data -v $(pwd):/backup alpine tar czf /backup/pgdata-backup-$(date +%Y%m%d).tar.gz /data - Remove the volume:
docker volume rm pgdata - Restart:
docker compose up -d postgres
This two-step backup-before-delete habit has saved me during schema migration disasters. For comprehensive backup strategies applicable to both Docker and bare-metal deployments, see my PostgreSQL backup and restore with pg_dump guide.
How do you initialize schemas and seed data automatically?
The official PostgreSQL Docker image executes files in /docker-entrypoint-initdb.d/ alphabetically, but only on first container start when the data directory is empty. This catches many developers off guard when they modify init scripts and expect changes to apply on restart.
Structuring initialization scripts
# init/01-extensions.sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
# init/02-schema.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
# init/03-seed.sql
INSERT INTO users (email) VALUES
('[email protected]'),
('[email protected]')
ON CONFLICT DO NOTHING; Mount the entire directory rather than individual files:
volumes:
- ./init:/docker-entrypoint-initdb.d:ro Applying schema changes after initial setup
Since init scripts don't re-run, use one of these approaches for ongoing development:
- Migration tools (recommended): Flyway, Alembic, or Knex migrations run idempotently on every app start. This mirrors production deployment patterns exactly.
- Manual psql execution:
docker exec -i pg-dev psql -U devuser -d app_dev < migrations/004-add-profiles.sql - Volume reset: Only for early prototyping when data doesn't matter. Follow the safe reset procedure above.
What are common performance and security pitfalls in local PostgreSQL containers?
Development databases often accumulate bad habits that migrate to production. Addressing these early prevents costly refactors later.
| Pitfall | Risk | Fix |
|---|---|---|
Using latest tag | Unexpected major version upgrades break queries | Pin to specific version: postgres:16.4-alpine |
| No resource limits | Runaway queries starve host CPU/RAM | Add deploy.resources.limits in compose |
| Default postgres superuser | App connects as superuser, masks permission bugs | Create dedicated app user with minimal privileges |
| Logging to stdout only | Lost query history after container restart | Configure log_destination + volume mount |
| Exposing port 5432 publicly | Accidental internet exposure in cloud dev environments | Bind to localhost: "127.0.0.1:5432:5432" |
Resource limits for development safety
services:
postgres:
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
memory: 512M These limits prevent a single unoptimized query from freezing your laptop. Adjust based on your dataset size; 2GB handles most development workloads comfortably. For teams working with large datasets or AI embeddings, consider the trade-offs discussed in vector databases for RAG: pgvector vs Pinecone before scaling up local resources.
Creating a least-privilege application user
Add this to your init scripts instead of connecting as POSTGRES_USER:
-- init/00-app-user.sql
CREATE USER app_user WITH PASSWORD 'app_pass_secure';
GRANT CONNECT ON DATABASE app_dev TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO app_user; Update your application connection string to use app_user. This catches permission errors locally that would otherwise surface only during production deployment audits.
When should you avoid Docker for local PostgreSQL development?
Docker isn't universally optimal. Recognizing when to choose alternatives saves debugging time and aligns with compliance requirements.
Scenarios favoring managed or native installations
- SOC 2 / ISO 27001 audit scope: If your local environment falls under audit controls (common in fintech and healthtech), Docker containers lack the access logging and change tracking auditors expect. Use a managed service with audit trails even for development.
- Heavy extension development: Writing custom C extensions or modifying PostgreSQL source requires build toolchains that complicate containerization. Native installation with version managers like
asdforpgenvis more practical. - Performance-critical benchmarking: Docker adds measurable overhead (~3-8% on Linux, higher on macOS/Windows). For accurate query plan analysis or load testing, use bare metal.
- Team-wide shared state: When multiple developers need identical datasets for integration testing, a centralized staging database with read replicas beats individual containers. See PostgreSQL replication and high availability for architecture patterns.
Hybrid approach for regulated teams
Many Nepal-based fintech clients I work with use Docker for feature development but validate against a managed RDS instance before merging. This balances velocity with compliance. Document which environment is authoritative in your README and CI pipeline configuration.
Running PostgreSQL in Docker for Development Effectively
Running PostgreSQL in Docker for development delivers isolation, reproducibility, and production parity when configured correctly. Pin your image version, always use named volumes, implement health checks, create least-privilege users, and structure init scripts for maintainability. Avoid Docker when compliance, performance benchmarking, or extension development demands native installations. Your future self — and your teammates inheriting the project — will thank you for getting these foundations right now.
If your team needs help designing compliant development environments, optimizing database performance, or preparing infrastructure for SOC 2 audits, reach out through my contact page. I regularly assist Nepal-based startups and global remote teams with production-grade PostgreSQL architectures that scale safely from local development to multi-region deployments.