Oct 30, 20265 min read

How to Run PostgreSQL Locally for Development

Four ways to get a local Postgres for your app: started automatically by Encore, Docker Compose, Homebrew or Postgres.app, and Testcontainers, with the PostgreSQL 18 Docker volume change that loses data if you miss it.

Running Postgres locally gets harder when two projects need different major versions, a teammate's schema drifts from yours, or a container upgrade quietly stops persisting data. The choice of how you run it decides how often those happen.

This guide compares the four common setups, starting with Encore, where encore run starts Postgres locally from a declaration in your code.

Local Postgres options at a glance

EncoreDocker ComposeHomebrew or Postgres.appTestcontainers
SetupDeclare the database in codeA compose file per projectA system installA library in your tests
Version per projectManaged by EncorePinned in the image tagOne version per installPinned in test code
MigrationsApplied on encore runYour migration tool, run by handYour migration tool, run by handRun in test setup
Same in CIYes, encore testYesas a service containerNoYes
Best forApps where the database ships with the codeTeams on any stackQuick native access on a MacIntegration tests only

Each option is covered below, starting with Encore.

Getting Postgres from a declaration in code with Encore

Encore combines an open source infrastructure SDK for TypeScript and Go with a platform that provisions and deploys into your own AWS or GCP account. A database is an object in your code with a folder of migrations:

-- tasks/migrations/1_create_tasks.up.sql CREATE TABLE tasks ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, done BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );
// tasks/tasks.ts import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; // encore run starts this database in Docker and applies ./migrations. const db = new SQLDatabase("tasks", { migrations: "./migrations" }); interface Task { id: number; title: string; done: boolean; } export const create = api( { expose: true, method: "POST", path: "/tasks" }, async ({ title }: { title: string }): Promise<Task> => { const task = await db.queryRow<Task>` INSERT INTO tasks (title) VALUES (${title}) RETURNING id, title, done`; return task!; }, ); export const list = api( { expose: true, method: "GET", path: "/tasks" }, async (): Promise<{ tasks: Task[] }> => { const tasks = await db.queryAll<Task>` SELECT id, title, done FROM tasks ORDER BY created_at DESC LIMIT 50`; return { tasks }; }, );
Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

encore run starts Postgres locally in Docker, creates the tasks database, applies the migrations and connects the service. A new teammate gets a working database with the same schema by cloning the repository and running one command, with no compose file or connection string in .env. Encore uses the encoredotdev/postgres image, which ships with extensions such as pgvector and PostGIS.

The CLI covers the daily tasks:

~/app
# psql into the local tasks database$ encore db shell tasks # A connection string for a GUI client or script$ encore db conn-uri tasks # Reset the local tasks database to a clean state$ encore db reset tasks

encore test runs your tests against separate test databases configured to skip fsync and use an in-memory filesystem, so tests do not touch your development data. The same declaration becomes RDS on AWS or Cloud SQL on GCP when you deploy to your own cloud account, so local and production run the same migrations.

Option 1: Docker Compose

A compose file pins the version and every developer gets the same database:

# compose.yaml services: db: image: postgres:18 environment: POSTGRES_USER: app POSTGRES_PASSWORD: app POSTGRES_DB: app ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql healthcheck: test: ["CMD-SHELL", "pg_isready -U app"] interval: 2s retries: 10 volumes: pgdata:

Note the volume path. Starting with PostgreSQL 18, the official image sets PGDATA to /var/lib/postgresql/18/docker and declares its volume at /var/lib/postgresql. Many existing compose files and tutorials mount /var/lib/postgresql/data, the path for 17 and earlier. On 18 that mount is not where Postgres writes, so the data lands in an anonymous volume and is gone the next time the container is recreated.

Two more details: scripts in /docker-entrypoint-initdb.d run only when the data directory is empty, so changing them does nothing to an existing volume, and the POSTGRES_* variables are likewise only applied on first initialization. You still run migrations yourself and keep the connection string in sync with the app.

Option 2: Homebrew or Postgres.app

On a Mac, brew install postgresql@18 followed by brew services start postgresql@18 gives you a native server, and Postgres.app does the same with a menu bar app and several versions side by side. Both start instantly and avoid Docker's file system overhead.

The version belongs to the machine, not the project, so two projects on different major versions, or a teammate on Linux, means someone runs a version that production does not.

Option 3: Testcontainers

Testcontainers starts a throwaway Postgres container from your test code and tears it down afterwards, with libraries for Go, Node.js, Java and other languages. It is the right tool for integration tests in CI, since each run starts clean. It is not a development database: the data disappears with the test run, and starting a container per test suite adds seconds.

Good to know

  • Match the local major version to production. Features like MERGE ... RETURNING (PostgreSQL 17) or the async I/O in 18 make version drift a real source of "works on my machine" bugs.
  • Keep a seed script or seed migration for development data, so reset gives everyone the same starting point.
  • If port 5432 is taken by another project, map the container to a different host port instead of stopping the other database.

How to choose

Use Encore when the database should come with the code: declared next to the service, started and migrated by encore run, isolated in tests and provisioned as RDS or Cloud SQL in your own account on deploy. Use Docker Compose for a project on another stack, with the PostgreSQL 18 volume path. Use Homebrew or Postgres.app for quick native access on one machine, and Testcontainers for integration tests in CI.

Frequently asked questions

What is the easiest way to run PostgreSQL locally?

For most projects, a container. Docker Compose with the official postgres image gives every developer the same version. With Encore you skip the Compose file: declaring new SQLDatabase("tasks", { migrations: "./migrations" }) in code is enough, and encore run starts Postgres in Docker, creates the database and applies the migrations.

Why does my Postgres data disappear when I recreate the Docker container?

On PostgreSQL 18 and later, the official image moved its volume from /var/lib/postgresql/data to /var/lib/postgresql, and PGDATA is now /var/lib/postgresql/18/docker. A volume mounted at the old /var/lib/postgresql/data path is not where the data is written, so it is not persisted. Mount your named volume at /var/lib/postgresql instead.

Should I install PostgreSQL with Homebrew or use Docker?

Homebrew (brew install postgresql@18) or Postgres.app runs natively and starts fast, which suits one developer on one project. Docker pins the exact version per project and matches CI, which matters once several people or projects use different Postgres versions.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders