Sync the self-hosting stack

This commit is contained in:
2026-08-27 17:52:23 +02:00
parent af6710d849
commit 02ab18aff1
12 changed files with 1407 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
# One hostname, two backends.
#
# The API is routed STRAIGHT AT ITS SERVICE rather than through the web container's nginx, which is
# what that nginx does on Compose. Two reasons: it removes a hop from every API request and every
# websocket frame, and it makes the web pod a pure static server again — no dependency on resolving
# the API, nothing to keep in sync between an ingress and a config file.
#
# The consequence has to be applied, not just noted: there is now exactly ONE proxy rewriting
# X-Forwarded-For, so `TRUST_PROXY_HOPS` is 1. It is 2 in the compose files, and leaving it there
# would make `req.ip` a value the caller chooses. See 02-config.yaml.
#
# Set `externalTrafficPolicy: Local` on the ingress controller's own Service as well, or the node's
# SNAT will make every request in the world appear to come from a handful of addresses — which
# defeats every per-address limit in the application at once.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: jarvis
namespace: jarvis
annotations:
# An agent is idle between operations and the server pings every 30s, so this only has to
# outlast a quiet period comfortably. Too short and a healthy fleet reconnects all day.
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# Matches BODY_LIMIT in apps/api/src/main.ts. A document may hold 400,000 characters, which is
# over a megabyte once accented text is UTF-8 encoded, and a diagram export posts back the SVG
# the browser rendered. Below this they are rejected as a bare 413 with no message.
nginx.ingress.kubernetes.io/proxy-body-size: "4m"
#
# NO affinity annotation, and none should be added. The reflex is understandable — there are
# websockets here — but stickiness cannot solve the problem it would be hired for: 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.
#
# And there is no polling to make sticky: both the client and the server pin
# transports: ["websocket"], so one logical socket is one TCP connection.
spec:
ingressClassName: nginx
tls:
- hosts: ["jarvis.example.com"]
secretName: jarvis-tls
rules:
- host: jarvis.example.com
http:
paths:
# Longest prefix first for readers; the controller sorts by specificity itself.
#
# The agent websocket has its own rule even though it sits under /api/, mirroring the
# split in apps/web/nginx.conf: it is the one path where an idle connection legitimately
# stays open for an hour between frames.
- path: /api/agents/ws
pathType: Prefix
backend:
service:
name: jarvis-api
port:
name: http
- path: /socket.io/
pathType: Prefix
backend:
service:
name: jarvis-api
port:
name: http
- path: /api/
pathType: Prefix
backend:
service:
name: jarvis-api
port:
name: http
# Everything else is the SPA, whose nginx answers index.html for any path it does not
# have a file for.
- path: /
pathType: Prefix
backend:
service:
name: jarvis-web
port:
name: http