Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Run PostgreSQL in Docker for Development

By Kokil Thapa | Last reviewed: September 2026

You need a clean PostgreSQL instance on your laptop without installing server packages on the host. The fastest path is to run PostgreSQL in Docker for development: one container, a named volume for data, and environment variables your app already understands. This guide walks through a setup I use on Laravel and Symfony projects, with copy-paste configs that survive team onboarding and CI parity. If you already use Docker for apps, adding Postgres takes minutes—not a weekend of local DBA work. Start with our Docker installation guide for Ubuntu if the engine is not running yet.

Why should you run PostgreSQL in Docker for local development?

Host-installed PostgreSQL works until it does not. Version drift, conflicting extensions, and leftover databases from old projects create friction. A container gives you PostgreSQL 18 today and PostgreSQL 17 tomorrow—without apt purge cycles.

On client projects I often maintain both MySQL and PostgreSQL apps. Docker keeps each project's database isolated. Your Laravel app on port 8000 talks to postgres-app-a. Your Symfony service talks to postgres-app-b. Neither touches the other's data directory.

Containers also mirror production more closely than XAMPP-style stacks. Managed hosts and VPS deployments run PostgreSQL as a dedicated service. Local Docker teaches the same connection string patterns: host, port, database, user, password. That alignment reduces "works on my machine" bugs during MySQL-to-PostgreSQL migrations.

Local Dev Stack with PostgreSQL in DockerHost Machine (macOS / Linux / WSL2)Docker EngineApp ContainerLaravel / SymfonyPostgreSQL 18Port 5432Named Volume: pgdata (persistent data)TCP 5432
Run PostgreSQL in Docker for development: app and database containers share a Docker network while data persists on a named volume.

Docker is not magic. You still need backups, sane credentials, and migration discipline. What changes is repeatability. A new developer runs docker compose up -d and gets the same major version you use in staging.

How do you start PostgreSQL with a single docker run command?

For a quick throwaway database, docker run is enough. Pin the major version. Never use the floating latest tag on shared projects.

Minimal one-liner

docker run -d \
  --name dev-postgres \
  -e POSTGRES_USER=app \
  -e POSTGRES_PASSWORD=secret \
  -e POSTGRES_DB=app_dev \
  -p 5432:5432 \
  -v pgdata_dev:/var/lib/postgresql/data \
  postgres:18

Connect from the host with any SQL client:

psql "postgresql://app:secret@127.0.0.1:5432/app_dev"

Flags worth understanding:

  • -v pgdata_dev:/var/lib/postgresql/data — stores cluster files in a Docker-managed volume. Without it, data dies when the container is removed.
  • -p 5432:5432 — publishes the port to localhost. Skip this if only sibling containers need access.
  • POSTGRES_* env vars — seed the superuser role and default database on first init only.

Verify the container is healthy before running migrations:

docker logs dev-postgres
docker exec dev-postgres pg_isready -U app -d app_dev

Official image behaviour is documented on Docker Hub's PostgreSQL page. Read the "Environment Variables" section once. It saves hours of guessing.

What is the best Docker Compose setup for PostgreSQL development?

Real teams outgrow one-liners. Docker Compose gives you a file in git, repeatable service names, and health checks. Pair it with the patterns in our multi-container Compose guide for local development.

services:
  postgres:
    image: postgres:18
    container_name: app_postgres
    restart: unless-stopped
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: ${DB_USERNAME:-app}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-secret}
      POSTGRES_DB: ${DB_DATABASE:-app_dev}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-app} -d ${DB_DATABASE:-app_dev}"]
      interval: 5s
      timeout: 5s
      retries: 10

volumes:
  pgdata:

Start it:

docker compose up -d postgres
docker compose ps

Wait until health shows healthy. Then run application migrations. On Laravel 13 with PHP 8.3+, your .env might look like this:

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=app_dev
DB_USERNAME=app
DB_PASSWORD=secret

When the app also runs in Compose, set DB_HOST=postgres instead. Docker DNS resolves the service name on the internal network. That pattern is covered in depth in Docker networking and volumes explained.

PostgreSQL Container Startup Sequence1. Pull Imagepostgres:182. Mount Volpgdata3. Init DBfirst run only4. Health OKpg_isreadyApp Layer (after healthcheck passes)Run Migrationsphp artisan migrateSeed Datafactories / fixturesdepends_on + healthy
Startup order when you run PostgreSQL in Docker for development: image pull, volume mount, first-run init, then app migrations after the health check passes.

App + database in one Compose file

For full-stack local work, add your PHP app service:

services:
  app:
    build: .
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DB_HOST: postgres
      DB_CONNECTION: pgsql
    volumes:
      - .:/var/www/html

  postgres:
    image: postgres:18
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app_dev
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app_dev"]
      interval: 5s
      retries: 10

volumes:
  pgdata:

Laravel Sail wraps a similar layout. If you standardise on Sail, read local Laravel dev with Sail and Docker alongside this page. Symfony projects follow the same Compose skeleton with different env var names.

How do docker run and Docker Compose compare for PostgreSQL dev?

Both launch the same image. The difference is workflow ergonomics and team scale.

Criteriadocker runDocker Compose
ReproducibilityCommand history or shell scriptsVersion-controlled YAML in the repo
Multi-service appsManual network and link flagsBuilt-in service DNS and depends_on
Health checksPossible but verbose CLIDeclarative healthcheck block
Env managementInline -e flags.env interpolation
Teardowndocker rm -f per containerdocker compose down
Best forQuick experiments, one-off importsDay-to-day team development

Verdict: use docker run to test an extension or major version bump. Use Compose for anything that survives beyond an afternoon. Most production-minded teams pick Compose plus Compose profiles and overrides to split dev, test, and CI configs.

How do you connect Laravel, Symfony, and CLI tools to the container?

Connection rules depend on where the client process runs. The matrix is simple but easy to get wrong.

  1. App on host, DB in DockerDB_HOST=127.0.0.1 and published port 5432.
  2. App in Docker, DB in Docker (same Compose project)DB_HOST=postgres using the service name.
  3. GUI client (DBeaver, pgAdmin, TablePlus)127.0.0.1:5432 with the credentials from Compose env.
  4. CI runner — service container named postgres on the job network; no host port publish required.

For Laravel, install the PHP pgsql extension and confirm the driver:

php -m | grep pgsql
php artisan db:show

Symfony uses Doctrine. Set DATABASE_URL in .env.local:

DATABASE_URL="postgresql://app:secret@127.0.0.1:5432/app_dev?serverVersion=18&charset=utf8"

Our PostgreSQL guide for Laravel developers covers migrations, JSON columns, and indexing patterns beyond connection strings. If you are choosing between engines, compare PostgreSQL vs MySQL for production before you commit schema design.

On booking systems like Adventure Third Pole Trek, PostgreSQL handled relational booking data with strict constraints. Local Docker let us test concurrent reservation logic without touching shared staging data.

Who Connects Where?App on Host127.0.0.1:5432App Containerhost: postgresGUI Client127.0.0.1:5432PostgreSQL Containerpostgres:18Internal port 5432
Connection hostnames when you run PostgreSQL in Docker for development: localhost for host processes, service name for sibling containers.

What are the common mistakes when running PostgreSQL in Docker locally?

Most failures I see in production debugging sessions start as innocent local shortcuts. Avoid these early.

Port 5432 already in use

Another Postgres instance—or a stale container—owns the port. Check with:

sudo ss -tlnp | grep 5432
docker ps --filter publish=5432

Remap the host port if needed: "5433:5432". Update DB_PORT=5433 in your app env.

Data loss after docker compose down

docker compose down -v deletes named volumes. That is correct for CI. It is catastrophic for a week's local seed data. Use plain down during normal work. Understand volume lifecycle in our volumes and networking article.

Wrong major version after image tag change

PostgreSQL cannot downgrade data directories. If you switch from 18 to 17, the container exits with a init error. Fix: backup with pg_dump, remove the volume, recreate on the target version. For production upgrade paths see PostgreSQL administration essentials.

Weak default passwords in shared repos

secret is fine for solo local work. Never reuse it in staging or production. Generate strong passwords with our password generator tool and keep secrets in .env files excluded from git.

Missing health checks cause race conditions

Migrations run before Postgres accepts connections. The app crashes once and sometimes leaves partial state. Add depends_on: condition: service_healthy or retry logic in entrypoint scripts.

PostgreSQL Docker TroubleshootingContainer won't start?Port conflictChange host portVersion mismatchDump and new volumeApp cannot connect?Fix DB_HOST valueWait for healthcheck
Quick decision paths when PostgreSQL in Docker for development fails: port conflicts, version mismatches, wrong hostnames, and startup races.

Performance tuning on macOS and WSL2

Docker Desktop file sharing can slow heavy write workloads. For large local imports, prefer docker exec -i with psql or run bulk loads inside the container filesystem. On Linux natively, performance is close to bare metal for typical dev datasets.

Resource limits matter when you run multiple containers. See how to limit Docker container resources before your laptop fan becomes the team soundtrack.

How do you seed, back up, and reset your development database?

Development databases are disposable in theory. In practice you want fast reset without mystery state.

Dump and restore

docker exec app_postgres pg_dump -U app -d app_dev -Fc > backup.dump
docker exec -i app_postgres pg_restore -U app -d app_dev --clean --if-exists < backup.dump

Custom format (-Fc) compresses well and restores selectively. Plain SQL dumps are easier to diff in git for small fixtures.

Reset everything

docker compose down
docker volume rm project_pgdata
docker compose up -d postgres
php artisan migrate:fresh --seed

Document the reset command in your project README. Future you—and every contractor—will follow it.

Init scripts for extensions

Need uuid-ossp, pg_trgm, or PostGIS locally? Mount SQL into /docker-entrypoint-initdb.d/:

volumes:
  - pgdata:/var/lib/postgresql/data
  - ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/01-init.sql
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

Init scripts run only on first volume creation. Changing them later requires a fresh volume or manual CREATE EXTENSION.

Validate JSON payloads during API work with the JSON formatter tool when debugging PostgreSQL JSONB columns alongside your HTTP layer.

Key Takeaways

  • Pin postgres:18 (or your team's target major) and store config in Docker Compose—not ad hoc shell history.
  • Always use a named volume for /var/lib/postgresql/data; bind mounts are rarely worth the permission pain on macOS.
  • Set DB_HOST=127.0.0.1 for host apps and DB_HOST=postgres for containerised apps on the same Compose network.
  • Add a healthcheck with pg_isready so migrations never race a starting database.
  • Never run docker compose down -v unless you intend to wipe local data completely.
  • Keep dev credentials in .env, dump before major version changes, and match production extensions in init scripts.

People Also Ask

Can I run PostgreSQL in Docker for development on Windows?

Yes. Docker Desktop with WSL2 backend is the practical path on Windows 10 and 11. Install Docker inside Ubuntu on WSL2, publish port 5432, and connect from Windows tools via localhost. File performance is better when the project lives inside the WSL filesystem—not on C:\ mounted volumes.

Is PostgreSQL in Docker safe for production?

Docker is fine as a runtime wrapper in production when you operate it like any other Postgres deployment: persistent volumes, monitored backups, replication, and resource limits. This article targets local development only. Production topology belongs on managed services or carefully administered VMs—see the Linux system administration service for ops-heavy deployments.

Which PostgreSQL version should I use in 2026?

PostgreSQL 18 is the current major release. PostgreSQL 17 remains widely deployed and fully viable. Pick the version your staging and production environments use. Mixed majors between local and prod invite subtle compatibility surprises in queries and extensions.

Does Laravel Sail include PostgreSQL?

Sail supports PostgreSQL through Compose stubs. Run php artisan sail:install and select pgsql. Sail publishes a prewired Compose file similar to the examples here. It is the fastest Laravel-specific path if your whole team already standardises on Sail.

Ship your next project with confidence

You now have a repeatable way to run PostgreSQL in Docker for development: pinned images, persistent volumes, health checks, and connection rules that work for Laravel, Symfony, and GUI clients. Start with Compose in your repo today, align the major version with staging, and treat local data like it matters—because schema bugs caught early save production firefights later. Building something that needs PostgreSQL-backed workflows, booking logic, or a full enterprise application stack? Review our eCommerce portfolio work or contact us to talk through architecture before you commit to schema decisions.

Frequently Asked Questions

Host-installed PostgreSQL creates version drift, conflicting extensions, and leftover databases from old projects. A container gives you an isolated instance per app—postgres-app-a for one Laravel project, postgres-app-b for a Symfony service—without apt purge cycles. You can switch from PostgreSQL 18 to 17 by changing the image tag. Docker also mirrors production connection patterns: host, port, database, user, password. That alignment cuts down on works-on-my-machine bugs, especially during MySQL-to-PostgreSQL migrations. A new developer runs docker compose up -d and gets the same major version you use in staging.

Run the official postgres:18 image with POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB environment variables, port 5432 published, and a named volume mounted at /var/lib/postgresql/data. Pin the major version; never use the floating latest tag on shared projects.

Store a compose.yaml in git with a postgres service using image postgres:18, restart unless-stopped, and environment variables interpolated from .env: POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB. Mount a named volume at /var/lib/postgresql/data and publish port 5432. Add a healthcheck running pg_isready against your user and database, with interval 5s, timeout 5s, and retries 10. Start with docker compose up -d postgres, wait until docker compose ps shows healthy, then run application migrations. For full-stack work, add your PHP app with depends_on using condition service_healthy and DB_HOST set to the postgres service name.

Both launch the same image; the difference is workflow ergonomics. docker run suits quick experiments, one-off imports, or testing an extension or major version bump. Docker Compose gives version-controlled YAML, built-in service DNS, declarative health checks, .env interpolation, and simpler teardown with docker compose down. For anything that survives beyond an afternoon or involves multiple services, Compose is the practical choice. Most production-minded teams use Compose with profiles and overrides to split dev, test, and CI configs. Reproducibility from git beats command history or shell scripts when onboarding teammates.

Install the PHP pgsql extension and confirm it with php -m | grep pgsql. When the Laravel app runs on the host and PostgreSQL runs in Docker, set DB_CONNECTION=pgsql, DB_HOST=127.0.0.1, DB_PORT=5432, and your credentials in .env. When both app and database share a Compose file, set DB_HOST=postgres so Docker DNS resolves the service name on the internal network. Run php artisan db:show to verify the connection. Wait for the container healthcheck to pass before running migrations, or use depends_on with condition service_healthy to avoid startup race conditions.

Use DB_HOST=127.0.0.1 when the application process runs on the host and connects through the published port 5432. Use DB_HOST=postgres, matching the Compose service name, when the app container shares the same Compose network as the database.

Symfony uses Doctrine with DATABASE_URL in .env.local, for example postgresql://app:secret@127.0.0.1:5432/app_dev with serverVersion=18 and charset=utf8 when the app runs on the host. GUI clients like DBeaver, pgAdmin, or TablePlus connect to 127.0.0.1:5432 using the same credentials from your Compose environment block. In CI, use a service container named postgres on the job network without publishing a host port. The connection matrix is simple but easy to get wrong: host processes use localhost, sibling containers use the service name, and CI runners use the internal service hostname.

Port 5432 conflicts with another host Postgres instance or a stale container—check with ss -tlnp or docker ps and remap to 5433 if needed. Running docker compose down -v deletes named volumes and wipes a week of seed data; use plain down during normal work. Switching image tags from PostgreSQL 18 to 17 fails because Postgres cannot downgrade data directories. Weak passwords like secret are fine solo but must never reach staging. Missing healthchecks let migrations race a starting database, sometimes leaving partial schema state. Wrong DB_HOST values between host and container apps cause connection refused errors that waste debugging time.

Another PostgreSQL instance on the host, or a stale container, already owns port 5432. Check what is listening with sudo ss -tlnp | grep 5432 and list containers with docker ps --filter publish=5432. Stop the conflicting service or remove the old container. If you need both running, remap the host port in Compose to something like 5433:5432 and update DB_PORT=5433 in your application .env. This is one of the first things to check when a freshly started container exits immediately or your app reports connection refused on localhost.

Dump with docker exec app_postgres pg_dump -U app -d app_dev -Fc, redirecting output to backup.dump. The custom format compresses well and restores selectively. Restore with docker exec -i app_postgres pg_restore -U app -d app_dev --clean --if-exists, reading from backup.dump. Plain SQL dumps are easier to diff in git for small fixtures. Always dump before changing major image tags, because PostgreSQL cannot downgrade an existing data directory. Treat local data as worth keeping even though dev databases are disposable in theory—schema bugs caught early save production firefights later.

Run docker compose down, then docker volume rm project_pgdata using your actual volume name, then docker compose up -d postgres. Once the healthcheck passes, run php artisan migrate:fresh --seed for Laravel projects. Document this sequence in your project README so future you and contractors follow the same steps. Only use docker compose down -v when you intentionally want to wipe all local data, such as in CI pipelines. For day-to-day work, plain docker compose down preserves the named volume and your seed data survives container recreation.

Mount an SQL file into /docker-entrypoint-initdb.d/, for example ./docker/postgres/init.sql mapped to /docker-entrypoint-initdb.d/01-init.sql, alongside your named data volume. Inside the script, run CREATE EXTENSION IF NOT EXISTS for each extension you need. Init scripts execute only on first volume creation. If you add or change extensions later, either recreate the volume after a pg_dump backup or run CREATE EXTENSION manually against the running container. Match the extensions your staging and production environments use so local query behaviour stays consistent.

Yes. Docker Desktop with the WSL2 backend is the practical path on Windows 10 and 11. Install Docker inside Ubuntu on WSL2, publish port 5432, and connect from Windows tools via localhost. Keep your project files inside the WSL filesystem rather than on C:\ mounted volumes, because file performance is noticeably better there. Heavy write workloads on Docker Desktop file sharing can still feel slower than native Linux, so for large local imports prefer docker exec -i with psql or run bulk loads inside the container filesystem rather than through slow bind mounts.

Docker works as a runtime wrapper in production when operated with persistent volumes, monitored backups, replication, and resource limits. This article targets local development only; production belongs on managed services or carefully administered VMs.

Use PostgreSQL 18 via the postgres:18 image. PostgreSQL 17 remains widely deployed and viable. Always match the major version your staging and production environments run.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: