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 --prodEmits 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 myappOptional: -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_appProd 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:
| 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 | — |
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.
| Endpoint | Role in OKE | Swarm |
|---|---|---|
GET /_/ready | Kernel readiness (booting → orphan_scan → ready) | Use this — mesh + rolling updates wait; start_period: 60s covers orphan resume |
GET /health | App 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):
- Stops accepting new connections
- Releases Clock cron leases and Journal run leases held by this instance
- 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:
| 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) | Horizontally scaled compose / Swarm stacks | 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.comInclude 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
| 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
Build and tag first (docker build -t oke-<app>:latest …). Swarm runs the
image: tag only — never compose build:.
Confirm compose.prod.yml is in the -c list and the task health-checks
/_/ready. Without that HEALTHCHECK, Swarm 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).
Generated networks.oke.driver is bridge (fine for single-node swarm init).
Multi-node: set driver: overlay in compose.override.yml.
Learn more
- Deployment — choose compose, Swarm, or Kubernetes
- Docker — plain compose without Swarm
- Reverse proxy — Caddy / Traefik in full
- Kubernetes — Deployment, probes, Ingress
- CLI —
oke docker --prod,oke start - Store — PgDog and facets
- Flow — durable journal
Next
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.
Kubernetes
Run an OKE app as a plain Deployment — PgDog, readiness vs liveness probes, graceful SIGTERM, shared drivers, and honest multi-instance limits.