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 --prodWrites 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):
| Concern | Driver | Shared backend |
|---|---|---|
| SQL / durable journal / clock | postgres | Postgres (optionally via PgDog) |
| Gate rate counters | redis (drivers.store.kv) | Redis / Valkey / Dragonfly (images["store.kv"]) |
| Files | s3 | S3-compatible object store |
| Signal | see 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 -dPass 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
| Endpoint | Role | On plain compose |
|---|---|---|
GET /_/ready | Kernel readiness (booting → orphan_scan → ready) | compose.prod.yml HEALTHCHECK; load balancers should wait |
GET /health | App 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):
- Stops accepting new connections
- Releases Clock cron leases and Journal run leases held by this instance
- 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:
| Choice | Best for | How routing works |
|---|---|---|
Caddy (caddy:2-alpine) | Single instance, simplest automatic HTTPS | Generated Caddyfile → reverse_proxy app:6530 |
Traefik (traefik:v3.3) | docker compose up --scale app=N | Docker labels on app; replicas auto-discovered and load-balanced |
images: {
// …
proxy: "caddy:2-alpine", // or "traefik:v3.3"
},OKE_PROXY_HOST=app.example.com
OKE_PROXY_ACME_EMAIL=admin@example.comRe-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
| Surface | Multi-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 s3 | Shared |
Store files fs | Per-instance filesystem — boot warns; use s3 when scaled |
Signal redis | Emit relays to Redis; consume / live / drain are process-local outbox until Streams consume ships |
| Channel suppression / consent / receipts | Default stores are process-local — opt-out on replica A is invisible to B until a durable driver ships |
Troubleshooting
You still have 6530:6530 on app. Confirm images.proxy is set and
compose.proxy.yml is in the -f list so derivation omitted the host bind.
Confirm compose.prod.yml is in the -f list and the healthcheck hits
/_/ready. Without it, compose treats “container started” as ready.
drivers.store.kv must be redis with REDIS_URL set. A missing URL fails
boot (no silent memory fallback). Declaring memory for local is fine;
docker/prod should stay on redis.
drivers.store.files: "fs" is single-host. Switch docker/prod to s3
(create-oke templates already do).
Serve validates Host. Add your public hostname to allowedHosts
(Security).
Learn more
- Deployment — choose compose, Swarm, or Kubernetes
- Reverse proxy — Caddy / Traefik in full
- Docker Swarm —
docker stack deployon the same files - Kubernetes — Deployment, probes, Ingress
- CLI —
oke docker,oke docker --prod - Store — PgDog and facets
- Security —
allowedHosts
Next
Deployment
Choose plain docker compose, Docker Swarm, or Kubernetes — then follow that path's complete page for PgDog, probes, graceful shutdown, and TLS.
Docker Swarm
Deploy the generated compose stack with docker stack deploy — PgDog, HEALTHCHECK readiness, graceful SIGTERM, known multi-instance limits, and optional Caddy or Traefik.