Deployment

Docker

Deploy with plain docker compose — shared drivers, PgDog, readiness, graceful SIGTERM, and opt-in Caddy or Traefik when you need TLS without an orchestrator.

Plain docker compose is the simplest production shape: single or few app instances on one host (or a small set you manage yourself). Derive with oke docker --prod; opt into a proxy only when you need HTTPS at the edge.

The one rule

Treat every app container as replaceable. Put durable state in Postgres / Redis / S3, wait on GET /_/ready, give SIGTERM time to release leases, and leave images.proxy unset until you need TLS or --scale app=N.

Quick start

Derive compose

oke docker --prod

Writes Dockerfile, compose.yml, per-role layers, and compose.prod.yml. Also emits compose.all.yml (same layers merged) — optional single file. Without a proxy, the app still publishes port 6530.

Share backing services

Pin docker/prod drivers to shared stores (create-oke templates already do):

ConcernDriverShared backend
SQL / durable journal / clockpostgresPostgres (optionally via PgDog)
Gate rate countersredis (drivers.store.kv)Redis / Valkey / Dragonfly (images["store.kv"])
Filess3S3-compatible object store
Signalsee known limits below

Bring the stack up

Default — organized per-role files (oke docker prints the -f order):

cd docker
docker compose -f compose.yml -f compose.store.sql.yml \
  -f compose.pgdog.yml -f compose.prod.yml up -d

Pass every -f file listed. For TLS at the edge, see Reverse proxy, then include compose.proxy.yml.

Single-file alternative: docker compose -f compose.all.yml up -d (add -f compose.override.yml if you use one).

PgDog pooling

Bun.SQL defaults to 10 connections per process — fine for one instance. Scale out and N × pool can exceed Postgres max_connections.

When both store.sql and pgdog are pinned (docker/prod default in templates), DATABASE_URL points at PgDog on port 6432 — transaction pooling, wire-protocol transparent to Bun.SQL / Drizzle. No app code changes.

Why PgDog. Transaction pooling fixes the connection math. Naive poolers can leak session state (SET, RLS vars, LISTEN/NOTIFY) across clients; PgDog re-applies those under transaction mode.

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

Redis-protocol images (Redis · Valkey · Dragonfly)

images["store.kv"] defaults to Redis (most mature). Pin Valkey or Dragonfly the same way — driver id stays redis, same REDIS_URL. Image table + one-line licenses: Store · KV.

Readiness and liveness

EndpointRoleOn plain compose
GET /_/readyKernel readiness (bootingorphan_scanready)compose.prod.yml HEALTHCHECK; load balancers should wait
GET /healthApp liveness (create-oke ships one) — cheap “process alive”External monitors only — not orphan-scan aware

/_/ready returns 200 { ready: true } only after boot and the durable orphan scan finish. While booting or scanning it returns 503 { ready: false, reason: "booting" | "orphan_scan" }.

Consequence: orphan scan delays “healthy” (and traffic) on purpose so a new replica is not hit mid-takeover.

Graceful shutdown

On SIGTERM / SIGINT, installGracefulShutdown (wired by oke dev’s app runner; call it next to createBunRuntime().serve in custom entries):

  1. Stops accepting new connections
  2. Releases Clock cron leases and Journal run leases held by this instance
  3. Closes element runtimes and exits

Signal message leases have no proactive release today — survivors reclaim after the visibility TTL (default 30s). compose.prod.yml sets stop_grace_period: 30s so the release path can finish (≥ typical Clock / Journal / Signal lease TTL).

Reverse proxy

Omit images.proxy until you need HTTPS or horizontal scale without fighting a host port bind. Then pick one:

ChoiceBest forHow routing works
Caddy (caddy:2-alpine)Single instance, simplest automatic HTTPSGenerated Caddyfilereverse_proxy app:6530
Traefik (traefik:v3.3)docker compose up --scale app=NDocker labels on app; replicas auto-discovered and load-balanced
oke.config.ts
images: {
  // …
  proxy: "caddy:2-alpine", // or "traefik:v3.3"
},
docker/.env.docker
OKE_PROXY_HOST=app.example.com
OKE_PROXY_ACME_EMAIL=admin@example.com

Re-run oke docker --prod. Edge publishes 80/443; app stays internal. Set allowedHosts to your public hostname (Security).

Traefik never mounts raw docker.sock — only a filtered tecnativa/docker-socket-proxy on the internal network. Full custom-domain, Let's Encrypt, and socket-proxy rationale → Reverse proxy.

Known multi-instance limits

SurfaceMulti-instance today
Clock (postgres / shared file)Shared leader election
Journal (postgres / shared file)Shared durable runs + orphan resume
Gate rates (drivers.store.kv: redis)Shared counters
Store SQL / KV redis / files s3Shared
Store files fsPer-instance filesystem — boot warns; use s3 when scaled
Signal redisEmit relays to Redis; consume / live / drain are process-local outbox until Streams consume ships
Channel suppression / consent / receiptsDefault stores are process-local — opt-out on replica A is invisible to B until a durable driver ships

Troubleshooting

Learn more

Next

On this page