Sync the self-hosting stack

This commit is contained in:
2026-10-21 12:12:12 +02:00
parent e210b4db11
commit 6a2b67364b
13 changed files with 263 additions and 134 deletions
+15 -6
View File
@@ -158,6 +158,10 @@ Two consequences worth knowing:
- **The previous run's Job object, and its logs, are gone** by the time the next one starts. Ship
the logs if you need them after the fact.
- **A migration waiting on the schema lock is ended by its pod, not its Job.** Under Argo,
deleting the `jarvis-migrate` Job leaves it `Terminating` behind the hook's finalizer, its pod still
waiting and the sync waiting for good. Delete the pod instead — it stops in about a second — then
end the sync's operation (`argocd app terminate-op <application>`).
- **Every sync runs the migration.** That is safe — the phase is idempotent and holds a Postgres
advisory lock — and it is why the example Application leaves `automated` sync OFF. A commit that
applies a schema change with nobody watching turns the one thing this directory works hardest to
@@ -227,15 +231,16 @@ back.
**`TRUST_PROXY_HOPS` is one hop here, not two.** The Compose files say 2 because a request passes
your TLS terminator and then the web container's nginx. Here the Ingress routes `/api/` straight at
the api Service, so there is exactly one proxy rewriting `X-Forwarded-For`. Left at 2, `req.ip`
becomes **chosen by the caller**: anyone can send `X-Forwarded-For: 198.51.100.7` and have that
the api Service, so there is exactly one proxy rewriting `X-Forwarded-For`. Left at 2, the client
address the api records becomes **chosen by the caller**: anyone can send `X-Forwarded-For: 198.51.100.7` and have that
written into session records, audit rows and the key the anonymous WebAuthn budget counts on.
**Do not add session affinity.** It is the reflex the moment websockets are involved and it solves
nothing here: the hard case is co-locating an operator's browser with an *agent's* websocket, which
arrives from a different network at a different time and whose id is not even known at the browser's
handshake. No ingress annotation can express that. It is handled in the application, over Redis.
Both ends pin `transports: ["websocket"]`, so there is no polling to make sticky in the first place.
Both ends speak WebSockets only — the api allows no other transport and the console skips
negotiation — so there is no polling to make sticky in the first place.
## On exactly three nodes
@@ -258,7 +263,9 @@ answer to wanting more is a fourth and fifth node, not a different configuration
## What a failover actually costs
Measured, not estimated.
Measured, not estimated — on the api releases before 1.0.0, which were written in TypeScript. The
readiness and liveness design below is unchanged in 1.0.0; the figures have not been measured again
since.
**Redis**, with its primary killed under a running api:
@@ -298,8 +305,10 @@ outcome. Run it again.
websockets, live runs and open shells, and CPU is a poor proxy for a load made of long-lived
connections. Raise api replicas deliberately.
- **A rollout still cuts what is in flight.** An assistant run is interrupted and resumed with a
note; a terminal is not resumable and says so; and the web bundle's content-hashed chunks are
per-image, so a browser holding the old shell can 404 on a lazy import during the rollout window.
note; a terminal is not resumable and says so; and the console's files are fingerprinted per
image, so during the rollout window a tab holding the old console can miss one of the two parts
it loads on demand (the Markdown parser, the QR code generator). It then shows the Markdown as
plain text and offers the reload.
- **Validation is not admission.** These manifests satisfy the `restricted` Pod Security Standard
that [`00-namespace.yaml`](00-namespace.yaml) enforces. If you edit them, note that a schema
validator cannot see that policy: a missing `seccompProfile` or a container without a