Deployment

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.

Swarm consumes the same files oke docker --prod already writes. It adds built-in orchestration (scale, rolling updates, routing mesh) with a lower learning curve than Kubernetes. Scale replaceable replicas; share durable drivers.

The one rule

Deploy with docker stack deploy. Docker’s single HEALTHCHECK targets GET /_/ready. Treat every task as replaceable — shared Postgres / Redis / S3, SIGTERM via stop_grace_period, honest Signal/Channel process-local limits.

Quick start

Derive the prod compose layers

oke docker --prod

Emits Dockerfile, per-role layers, compose.prod.yml (HEALTHCHECK/_/ready, deploy.*, stop_grace_period: 30s), and compose.all.yml (layers 1–3 merged). compose.override.yml is yours (never written by oke).

Build the app image, init Swarm, deploy

docker stack deploy ignores build — tag first (name matches compose.yml). Prefer the single merged file:

docker build -t oke-<app>:latest -f docker/Dockerfile .
docker swarm init
cd docker
docker stack deploy -c compose.all.yml myapp

Optional: -c compose.override.yml after that for local tweaks. Create secrets: names via docker secret create, or drop those keys in compose.override.yml while using .env.docker.

Scale and roll

docker service scale myapp_app=3
docker service update --image oke-<app>:latest myapp_app

Prod overlay: update_config (parallelism: 1, order: start-first, failure_action: rollback) and restart_policy.condition: on-failure. Tweak in compose.override.yml.

Share backing services

Pin docker/prod drivers to shared stores:

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

PgDog pooling

Bun.SQL defaults to 10 connections per process — fine for one replica. Scale the Swarm service 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.

HEALTHCHECK (not two probes)

Swarm has one Docker HEALTHCHECK — no separate readiness/liveness.

EndpointRole in OKESwarm
GET /_/readyKernel readiness (bootingorphan_scanready)Use this — mesh + rolling updates wait; start_period: 60s covers orphan resume
GET /healthApp liveness (create-oke)External monitors only — not a second native probe

/_/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. After start_period, failing /_/ready can mark the task unhealthy and hit restart_policy — Swarm cannot drain traffic without also counting toward restart.

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

Swarm sends SIGTERM on service update and scale-down the same way.

Signal message leases have no proactive release today — survivors reclaim after the visibility TTL (default 30s). compose.prod.yml sets stop_grace_period: 30s (peer of K8s terminationGracePeriodSeconds) so release can finish.

Reverse proxy

Swarm's routing mesh reaches tasks; it does not issue TLS certificates. For HTTPS on the same files, opt in to images.proxy:

ChoiceBest forHow routing works
Caddy (caddy:2-alpine)Single instance, simplest automatic HTTPSGenerated Caddyfilereverse_proxy app:6530
Traefik (traefik:v3.3)Horizontally scaled compose / Swarm stacksDocker 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

Include compose.proxy.yml in the -c list. Edge publishes 80/443. 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

  • Deployment — choose compose, Swarm, or Kubernetes
  • Docker — plain compose without Swarm
  • Reverse proxy — Caddy / Traefik in full
  • Kubernetes — Deployment, probes, Ingress
  • CLIoke docker --prod, oke start
  • Store — PgDog and facets
  • Flow — durable journal

Next

On this page