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.
Each option is covered below, starting 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 };
},
);
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:
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.
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.
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.
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.
MERGE ... RETURNING (PostgreSQL 17) or the async I/O in 18 make version drift a real source of "works on my machine" bugs.reset gives everyone the same starting point.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.
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.
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.
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.