diff --git a/docs/deploy.md b/docs/deploy.md index 2bdc40e..f2b340b 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -196,3 +196,105 @@ flowchart LR **Rotating a secret** is a host-side edit — update the file and the service picks it up on restart. No rebuild, and nothing you run: flag it and we handle the restart. + +## Supporting services live in your compose, not the fleet + +Need Redis? **Declare it in your own `compose.yaml`, as a sidecar.** There is no shared +Redis — nothing fleet-wide, nothing per-workspace, nothing ops provisions for you. A repo +that needs Redis brings its own; a repo that doesn't adds nothing. + +That's the whole point of the deploy model. Every service already runs in its own compose +project on its own network so that one service falling over can't touch another. A shared +Redis puts that coupling straight back: one process everything depends on, whose OOM, stray +`FLUSHALL`, single-threaded stall, or restart becomes *everyone's* outage. A sidecar shares +its owning repo's fate and nobody else's — and the isolation is free, because it rides the +per-project network you already get. No ACLs, no key-prefix discipline, no shared +credentials to manage. + +Your app talks to it with the [`redis` lib](libraries.md) from the suite (async, +config-free, kv/hash/ttl/pubsub), pointed at **`redis://redis:6379`** — the compose +**service name**, not a host port. The sidecar comes up auto-namespaced on your project's +network like every other container, exactly as the naming convention above describes. + +### Ephemeral or persistent — pick deliberately + +A sidecar Redis is **ephemeral by default**: restart it and the data is gone. That's +correct for some workloads and quietly destructive for others, so make the call on purpose. +Ask one question — *if this data vanished on a restart, would anything be lost?* + +- **No → ephemeral.** A scratch cache, a dedupe set, rate-limit counters, transient data + you can just re-fetch. Nothing to back up, nothing to grow. +- **Yes → persistent.** An outbound webhook or notification queue, a job queue, anything + that could be mid-flight when the process dies. Losing it drops real work. + +=== "Ephemeral (cache / throwaway)" + + Fine to lose on restart — no volume, no persistence, by design. + + ```yaml + redis: + image: redis:7-alpine + restart: unless-stopped + command: redis-server --save "" --maxmemory 256mb --maxmemory-policy allkeys-lru + # no volume: throwaway by design + ``` + +=== "Persistent (durable queue / state)" + + Survives restart, rebuild, and reboot — the append-only file lives on the host mounts + dir injected at deploy, the same `${MOUNTS_DIR}` mechanism described above. Redis just + uses it as its backing store. + + ```yaml + redis: + image: redis:7-alpine + restart: unless-stopped + command: redis-server --appendonly yes --appendfsync everysec + volumes: + - ${MOUNTS_DIR:-./mounts}/redis:/data # AOF persists on the host mounts dir + ``` + +The app connects the same way in both modes — `REDIS_URL: redis://redis:6379`. Only the +durability changes. + +!!! warning "Two things to know before you rely on a persistent sidecar" + **`--appendfsync everysec` can lose ~1 second of the newest entries** on a hard crash. + For a webhook queue that's an acceptable trade — just know it's there. Use + `--appendfsync always` if you genuinely cannot drop a single entry (safer, slower). + + **Data under the mounts dir is not backed up.** Mounts are excluded from the backup + pipeline, and that's *right* for a queue: a lost queue means some notifications didn't + fire, not that business data is gone. **That's the dividing line.** If losing this data + would actually hurt, it isn't queue or cache state — it's a system of record, and it + belongs in Postgres (which *is* backed up), not a local mount. + +!!! tip "Sometimes you don't need Redis at all" + For the dead-simple case — **one process draining its own queue** — a **SQLite file** + under the mounts dir does the job with no extra container: atomic commits, crash-safe, + zero infra. Reach for Redis when it's actually doing queue *things* (multiple workers, + blocking pops, streams / consumer groups, pub/sub) or when it's already in your compose + for something else. + +### The rules + +!!! danger "Don't do these" + - **Don't map Redis to a host port.** No `ports: - "6379:6379"`. Two repos both + grabbing host 6379 on the same box collide. Keep it internal to the compose network — + nothing exposed, nothing to collide. + - **Don't stand up a shared or fleet-wide Redis.** Per-repo means per-need. One Redis + per *project*, shared by that project's containers if a repo runs several — never one + per fleet. + - **Don't treat persistent Redis as a database.** Queues and caches, yes. A durable + system of record, no — that's Postgres, and unlike a mount it's backed up. + +**You can't reach another repo's Redis** — different project, different network. That's not +a restriction you have to work around; it's the isolation working *for* you. Nobody else's +service can touch your cache or drain your queue either, and you never have to think about +whose keys are whose. + +!!! quote "What about ACLs?" + A shared Redis *can* be secured — Redis 6+ ACLs scope users by command, key pattern, and + channel. But ACLs don't solve resource contention or the noisy-neighbour problem, and + they add real management burden, so the fleet uses per-repo sidecars instead. Reserve + ACLs for the rare case of a deliberately shared, durable, backed-up Redis run as actual + infrastructure.