Bun.SQL defaults to **10** connections per process. Scale to several app instances and
`N × pool` can exceed Postgres `max_connections`. PgDog sits in front of `store.sql` as a
transaction-pooling proxy — same wire protocol, no app code changes.

<Callout title="The one rule">
  Pin `pgdog` alongside `store.sql` and `DATABASE_URL` automatically points at the pooler on `:6432`
  instead of Postgres directly — Bun.SQL and Drizzle see no difference.
</Callout>

## Quick start

<Steps>

<Step>
### Pin both images

create-oke asks **Add PgDog connection pooling…?** (or pass `--pgdog`). Manually:

```typescript title="oke.config.ts"
images: {
  "store.sql": "postgres:18-alpine",
  pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.57",
},
```

</Step>

<Step>
### Derive compose

```bash
oke docker
```

Writes `./pgdog/pgdog.toml` (listen + one primary database) and
`./pgdog/users.toml` (same user/password/database as Postgres). The pooler
waits on `store-sql` health before start.

</Step>

<Step>
### Confirm the rewrite

```bash
echo "$DATABASE_URL"
# …@host:6432/db  ← pooler, not :5432
echo "$OKE_STORE_SQL_URL"   # still the direct Postgres URL when you need it
```

</Step>

</Steps>

## Required env

| Variable                                 | Role                                                             |
| ---------------------------------------- | ---------------------------------------------------------------- |
| `OKE_STORE_SQL_USER` / `PASSWORD` / `DB` | Copied into `pgdog/users.toml` for client + server auth          |
| `DATABASE_URL`                           | Rewritten to the pooler URL (`:6432`) when PgDog is in the stack |
| `OKE_PGDOG_URL`                          | Same value as pooled `DATABASE_URL` when both are present        |
| `OKE_STORE_SQL_URL`                      | Direct Postgres host — bypass the pooler for admin / migrations  |

PgDog itself has no separate credential env — it mounts the generated TOML files read-only.

## Data and backup

| Mount                                     | What it is                                                         |
| ----------------------------------------- | ------------------------------------------------------------------ |
| `./pgdog/pgdog.toml:/pgdog/pgdog.toml:ro` | Generated listen + `[[databases]]` (host `store-sql`, port `5432`) |
| `./pgdog/users.toml:/pgdog/users.toml:ro` | Generated `[[users]]` credentials                                  |

**No database state lives in the PgDog container.** Backing up PgDog means keeping those
two config files (they are regenerated by `oke docker`). Durable data stays on the
[Postgres](/docs/recipes/postgres) volume / dump.

## Production note

Pooling mode is `transaction` — set explicitly (also PgDog's upstream default). Naive
poolers can leak session state (`SET`, RLS vars, `LISTEN`/`NOTIFY`) across clients;
PgDog re-applies that state per transaction.

**Do not stack** PgDog in front of a managed pooler (Neon `-pooler`, Supabase Supavisor
`:6543`). Pick one pooler. Use `OKE_STORE_SQL_URL` (direct) for migrations and anything
that needs session features.

Read-replica routing (`BEGIN READ ONLY` → replica) is documented readiness in PgDog —
not wired into the generated stack yet.

## What the recipe configures

| Field          | Value                                                   |
| -------------- | ------------------------------------------------------- |
| Container port | `6432`                                                  |
| `dependsOn`    | `store-sql` healthy                                     |
| Healthcheck    | `pg_isready -h 127.0.0.1 -p 6432`, every 2s, 20 retries |
| Connection URL | `postgres://user:pass@host:6432/db`                     |

## Troubleshooting

<Accordions>
<Accordion title="pgdog unhealthy — pg_isready on :6432 fails">

Usually the backend is not ready, or `pgdog/users.toml` credentials do not match Postgres.
Confirm `store-sql` is healthy first, then re-run `oke docker` so TOML matches
`OKE_STORE_SQL_*`. Logs often show auth failures against `store-sql:5432`.

</Accordion>
<Accordion title="Session features break through the pooler">

Transaction mode does not preserve session-scoped state the way a direct connection
does. Point migrations / `LISTEN` / session `SET` at `OKE_STORE_SQL_URL` (direct
Postgres), keep `DATABASE_URL` on the pooler for app traffic.

</Accordion>
</Accordions>

## Learn more

- [Postgres](/docs/recipes/postgres) — the backend PgDog fronts
- [Store · SQL](/docs/elements/store#sql) — “Connection pooling is infrastructure”
- [Configuration](/docs/reference/configuration) — `images.pgdog` and compose layers

## Next

<Cards>
  <Card title="Postgres" description="The backend PgDog fronts." href="/docs/recipes/postgres" />
  <Card
    title="Supabase"
    description="Also Postgres wire — same pooler applies."
    href="/docs/recipes/supabase-docker"
  />
  <Card title="Caddy" description="TLS at the edge, once you need it." href="/docs/recipes/caddy" />
</Cards>
