Run PostgreSQL in Docker for Development

Khimananda Oli 9 min read Database
Run PostgreSQL in Docker for Development

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.

Host MachineApp (localhost:5432)Docker CLIDocker ContainerPostgreSQL 16/var/lib/postgresql/dataHealth CheckInit Scripts (/docker-entrypoint-initdb.d)Named Volumepgdata_devPersists acrossrestarts & rebuilds
Container architecture for running PostgreSQL in Docker for development with persistent named volumes and isolated networking

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 -alpine tag 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.

❌ Without Named Volumedocker compose downContainer removedAnonymous layer deletedALL DATA LOSTImage update, prune, or accidental rm = total reset✅ With Named Volume (pgdata)docker compose downContainer removedVolume persists in DockerDATA SAFEdocker compose up → reattaches existing volumeRecovery CommandsList volumes:docker volume lsInspect mount point:docker volume inspect pgdataBackup before destructive ops:docker run --rm -v pgdata:/data \alpine tar czf /backup/pg.tar.gz /data⚠ Never run docker system prune -v
Data persistence comparison: anonymous layers vs named volumes when running PostgreSQL in Docker for development

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:

  1. Stop the container: docker compose stop postgres
  2. 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
  3. Remove the volume: docker volume rm pgdata
  4. 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.

PitfallRiskFix
Using latest tagUnexpected major version upgrades break queriesPin to specific version: postgres:16.4-alpine
No resource limitsRunaway queries starve host CPU/RAMAdd deploy.resources.limits in compose
Default postgres superuserApp connects as superuser, masks permission bugsCreate dedicated app user with minimal privileges
Logging to stdout onlyLost query history after container restartConfigure log_destination + volume mount
Exposing port 5432 publiclyAccidental internet exposure in cloud dev environmentsBind 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.

Start: Need Local Postgres?Solo dev or small team?< 5 developers, no complianceYESNODocker Compose + Named Volume✓ Fast setup (< 5 min)✓ Zero host pollution✓ Matches prod runtimeManaged DB or K8s Operator✓ SOC2/ISO27001 audit trail✓ Automated backups & HA✓ Team-wide consistent stateAdd health checks + init scriptsUse CloudNativePG or RDS LocalBoth paths require: pinned versions, least-privilege users, backup verification
Decision framework for selecting the right method to run PostgreSQL in Docker for development based on team scale and compliance requirements

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 asdf or pgenv is 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.

Frequently Asked Questions

The official postgres image on Docker Hub is the standard choice. Use a specific version tag like postgres:17-alpine to ensure consistency across your team and CI pipelines instead of relying on the latest tag which changes unexpectedly.

Mount a named volume to /var/lib/postgresql/data using docker compose. This keeps your database files safe outside the ephemeral container filesystem, ensuring data survives stops, rebuilds, and updates without manual backups.

Yes, map port 5432 in your compose file. Connect via localhost and the mapped port using psql or any GUI client, ensuring no local Postgres instance conflicts with that port binding.

Define POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB environment variables in your docker-compose.yml. These initialize the cluster on first run only; changing them later requires deleting the existing volume and reinitializing.

No, use managed services like RDS or Cloud SQL for production. Docker setups lack automated failover, backup management, and performance tuning required for critical workloads, making them strictly appropriate for local development and testing environments.

Place .sql or .sh files in /docker-entrypoint-initdb.d via volume mount. PostgreSQL executes these alphabetically during initial cluster creation only, automating schema setup and seed data loading without manual intervention.

Check logs with docker compose logs postgres. Common causes include incorrect permissions on mounted volumes, missing environment variables, or port conflicts preventing the postmaster process from binding successfully during startup.

Update the image tag in docker-compose.yml then run docker compose up -d. Always test against a volume copy first since major version upgrades require pg_upgrade, not just swapping container images.

Set memory and CPU limits in compose to prevent runaway queries from freezing your laptop. A typical dev setup needs 512MB RAM and one CPU core, adjustable based on dataset size and query complexity.

Create a custom config file and mount it to /etc/postgresql/postgresql.conf. Alternatively, pass individual parameters as command arguments in docker-compose.yml to override defaults without maintaining full configuration files.

No, each developer should run their own isolated container. Sharing instances causes schema conflicts, data corruption risks, and permission issues that defeat the purpose of containerized development environments entirely.

Stop the container, remove the named volume with docker volume rm, then recreate it via docker compose up. This destroys all data but provides a clean slate when migrations or initialization scripts fail repeatedly.

Yes, mount certificate files and set ssl=on in configuration. However, most development workflows skip SSL overhead since traffic stays on localhost, reserving encrypted connections for staging and production environments only.

Enable pg_stat_statements extension and set log_min_duration_statement to capture slow queries. Access logs via docker compose logs or mount the log directory to analyze performance bottlenecks without attaching debuggers.

Use Docker Compose default bridge networking. Containers reference PostgreSQL by service name rather than localhost, providing DNS resolution and isolation while avoiding host port mapping conflicts across parallel development environments.