Skip to content
RELKIT
Esc
↑↓navigate↵open⌘Jpreview
On this page

Set up PostgreSQL in Docker

Start a local PostgreSQL database, scaffold a Drizzle service, validate its environment, and apply the first migration.

Connect an existing RELKIT app to PostgreSQL running in local Docker. This is the default database setup for a new module. The Orders tutorial remains a separate SQLite walkthrough; keep its database and migration history when continuing that app.

Pre-setup

You need a working minimal app, Bun, and Docker with Compose. Run these commands in the backend root. This walkthrough adds a database only when the app has no database service. For an existing service, retain its dialect, tables, credentials, and applied migrations.

Start PostgreSQL

Check the daemon, then create one local password:

docker version
openssl rand -hex 24

Add the following values to your untracked .env.local, replacing both placeholders with the same generated password. Preserve other entries. Port 5433 must be free; if you change it, change both the connection URL and Compose port mapping.

RELKIT_LOCAL_DATABASE_PASSWORD=PASTE_GENERATED_HEX
DATABASE_URL=postgres://relkit:PASTE_GENERATED_HEX@127.0.0.1:5433/relkit

Create compose.local.yaml, or merge this service and volume into your existing Compose file:

services:
  database:
    image: postgres:16-alpine@sha256:cf78e76683b9ca8c5733cbbdce6c9262b45b6767934dd0a95e671f9a0fc20685
    environment:
      POSTGRES_DB: relkit
      POSTGRES_USER: relkit
      POSTGRES_PASSWORD: ${RELKIT_LOCAL_DATABASE_PASSWORD:?Set a local database password}
    ports:
      - "127.0.0.1:5433:5432"
    volumes:
      - database:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U relkit -d relkit"]
      interval: 2s
      timeout: 3s
      retries: 30
volumes:
  database:

This pins the same PostgreSQL image used by RELKIT’s local job infrastructure. The named volume keeps data across stops; the published port accepts only loopback connections. Start it:

docker compose --env-file .env.local -f compose.local.yaml up -d --wait database
docker compose --env-file .env.local -f compose.local.yaml exec database pg_isready -U relkit -d relkit

Expect accepting connections. RELKIT has no docker(postgresql()) integration; Compose owns this database lifecycle. Changing POSTGRES_PASSWORD does not rotate an initialized volume’s password. Keep the original value across restarts.

Scaffold the database service

bun run relkit add database --orm drizzle --dialect postgresql

The command installs the selected Drizzle packages and declaration repair, creates src/services/database/service.ts, adds an items table in its schema directory, and creates drizzle.config.ts. It also merges the database environment declaration. It does not start PostgreSQL or apply migrations. See the command reference.

Check the environment contract

Keep DATABASE_URL: env.secret() in the generated src/platform/env.ts and pass that environment descriptor to defineApp({ env }) in relkit.config.ts. The same configuration can be expressed inline as this complete minimal example:

/** Validate the database connection before activating the Drizzle service. */
import { defineApp, defineEnv, env } from "@relkit/app/config";

export default defineApp({
  env: defineEnv({ DATABASE_URL: env.secret() }),
});

Merge the field into your app’s existing contract; retain its ID and other settings. The generated Drizzle client reads context.env.DATABASE_URL through its client({ env }) factory. There is no database profile to add to defineApp: the specialized service registers it. RELKIT_LOCAL_DATABASE_PASSWORD belongs to Compose and need not be exposed to application functions.

bun run relkit env check

Bun loads .env.local; .env.example is only a template. Drizzle Kit and the app must use the same DATABASE_URL. Keep production credentials out of this local file and browser bundles.

Generate and apply a migration

Review the scaffolded table, then generate the initial migration:

bun --bun drizzle-kit generate --name=initial

Review the SQL and snapshots in src/services/database/migrations. Apply the reviewed history to the local database and start the app:

bun run relkit db migrate
bun run check
bun run typecheck
bun run dev

Expect successful migration, checks, and server readiness. In another terminal, verify that the scaffolded table exists:

docker compose --env-file .env.local -f compose.local.yaml exec database psql -U relkit -d relkit -c '\dt'

Expect items in the table list. If you renamed the schema table, check its new name instead. check, build, and dev never apply migrations for you.

Before running application tests, update existing HTTP fixtures to import createTestApplication from @relkit/testing/drizzle and supply an explicit test environment. The harness does not inherit .env.local. Use a separate test database and apply its migrations; follow database testing before changing the fixtures.

Stop and resume without resetting data

Stop the application with Ctrl+C, then stop its database:

docker compose --env-file .env.local -f compose.local.yaml stop database

Run the earlier up -d --wait command to resume with the same volume and password. Keep your schema, migration history, and volume; do not remove volumes to resolve a migration error.

Continue with Drizzle services, schema definitions, or authentication setup.

Reference and source