PgDog
Postgres wire-protocol pooler — transaction mode on :6432, generated pgdog/ configs, DATABASE_URL rewrite.
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.
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.
Quick start
Pin both images
create-oke asks Add PgDog connection pooling…? (or pass --pgdog). Manually:
images: {
"store.sql": "postgres:18-alpine",
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.57",
},Derive compose
oke dockerWrites ./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.
Confirm the rewrite
echo "$DATABASE_URL"
# …@host:6432/db ← pooler, not :5432
echo "$OKE_STORE_SQL_URL" # still the direct Postgres URL when you need itRequired 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 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
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.
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.
Learn more
- Postgres — the backend PgDog fronts
- Store · SQL — “Connection pooling is infrastructure”
- Configuration —
images.pgdogand compose layers