feat: proxy entry for test-filius and filius on the external openresty

One server block for both names, the stack picked by host: test on :8081,
production on :8080. WebSockets with Upgrade and 3600 s, /blobs above F's
16 MiB so that F refuses a too large file itself (with CORS), no add_header.
The LXC address is a placeholder that stops openresty -t. Checked with nginx
in front of two real F instances.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 17:17:16 +00:00
co-authored by Claude Opus 5.5
parent 4b33e36580
commit 709146b58a
2 changed files with 236 additions and 0 deletions
+14
View File
@@ -51,5 +51,19 @@ des Editors, durch Kommas getrennt); `FILIUS_PORT` ist 8080, wenn nicht
gesetzt. Fehlt eine davon, startet er nicht. Ohne HTTPS (Entwicklerrechner) gesetzt. Fehlt eine davon, startet er nicht. Ohne HTTPS (Entwicklerrechner)
setzt `FILIUS_INSECURE_COOKIE=1` den Keks ohne `Secure`. setzt `FILIUS_INSECURE_COOKIE=1` den Keks ohne `Secure`.
## Betrieb
Zwei Docker-Stacks auf Server F, je mit eigener Datenbank, eigenem
`FILIUS_TICKET_SECRET` und `FILIUS_SERVICE_KEY`:
| Name | Port | gehört zu |
| --- | --- | --- |
| `test-filius.hub.mdg-hamburg.de` | 8081 | `test-dashboard` |
| `filius.hub.mdg-hamburg.de` | 8080 | `dashboard` |
`deploy/proxy.conf` ist der Eintrag für den externen OpenResty (in dessen
`conf.d`); vorher die Adresse von F für `filius-lxc.invalid` einsetzen.
`FILIUS_ORIGINS` ist jeweils der eigene Name mit `https://`.
Code, Kommentare und Testnamen sind englisch; sichtbare Texte stehen hinter Code, Kommentare und Testnamen sind englisch; sichtbare Texte stehen hinter
i18n-Schlüsseln (Deutsch/Englisch). i18n-Schlüsseln (Deutsch/Englisch).
+222
View File
@@ -0,0 +1,222 @@
# test-filius.hub.mdg-hamburg.de and filius.hub.mdg-hamburg.de → server F
#
# A **site snippet** for the `conf.d` of the external openresty, like the
# dashboard's `proxy.conf` and Scratch's `deploy/proxy/scratch.conf`: it goes
# **into** the existing `http{}` and declares nothing in the main context
# except two `upstream`s and two `map`s.
#
# Two environments, one machine
# -----------------------------
# Test and production are two Docker stacks on the same machine (the LXC
# container of server F), each with its own database, its own secrets and
# its own port:
#
# test-filius.hub.mdg-hamburg.de → :8081 (filius_test)
# filius.hub.mdg-hamburg.de → :8080 (filius_production)
#
# Each stack has **one** entrance, the port of its F process. F delivers
# everything on it: the editor's page and bundle, `/__auth`, `/svc/*`,
# `/blobs`, `/healthz` and the two WebSockets `/sync` and `/sim`. This vhost
# is therefore a pass-through and splits nothing up — except where the
# proxy's own defaults would get in the way (WebSockets, upload size).
#
# The two environments are one `server` block: the `map` below picks the
# stack by the name the browser asked for. Two blocks that only differ in
# one line drift apart; whoever later changes a limit in one forgets the
# other, and the test environment no longer tests what production runs.
#
# Which dashboard talks to which F: `test-dashboard` to `test-filius`,
# `dashboard` to `filius` — each with the ticket secret and service key of
# its own stack. A ticket from the test dashboard is refused in production
# (wrong secret) and that is intended.
#
# ── The one line every installation touches ───────────────────────────
#
# `filius-lxc.invalid` (twice, in the two `upstream`s) is the address of the
# LXC container of server F, which does not exist yet. The placeholder is
# chosen so that it **cannot** work: `.invalid` never resolves, and
# `openresty -t` stops with "host not found in upstream". Whoever puts the
# file in place without replacing it finds out before the reload, not from
# a user.
#
# The Docker stacks have to publish the ports on an address this openresty
# reaches, e.g. in the stack's compose file (F's own port stays 8080 in
# both containers, `FILIUS_PORT`):
#
# test: ports: ["<address>:8081:8080"]
# production: ports: ["<address>:8080:8080"]
#
# Check from the openresty:
#
# curl -s http://<address>:8081/healthz
# curl -s http://<address>:8080/healthz
#
# ── What the stacks need in their environment ─────────────────────────
#
# `FILIUS_ORIGINS` is the origin of the page the editor runs in, and that is
# this host: F refuses a WebSocket and a blob upload from any other origin.
#
# test: FILIUS_ORIGINS=https://test-filius.hub.mdg-hamburg.de
# production: FILIUS_ORIGINS=https://filius.hub.mdg-hamburg.de
#
# No `FILIUS_INSECURE_COOKIE`: TLS ends here, and the browser reaches F only
# via https. The cookie carries `Secure` and `SameSite=Lax`; it reaches F in
# the dashboard's frame because `*.hub.mdg-hamburg.de` is the same site as
# the dashboard.
#
# ── What must **not** stand here ──────────────────────────────────────
#
# No `add_header`, above all no `X-Frame-Options`: the editor lives in a
# frame of the dashboard, and the header would turn it into a white box.
# (An `add_header X-Frame-Options` in the `http{}` of this openresty would do
# the same to this host.)
#
# No CORS headers. F answers CORS for `/blobs` itself, against its
# `FILIUS_ORIGINS`; a second `Access-Control-Allow-Origin` from here makes
# the browser refuse the response.
upstream filius_test {
server filius-lxc.invalid:8081;
}
upstream filius_production {
server filius-lxc.invalid:8080;
}
# Which stack a host goes to. `$host` is the name without port, in lower
# case. The `server_name` below lets only these two names in, so the
# `default` never decides anything; it is there so that no request ends in a
# `proxy_pass` to nothing.
map $host $filius_upstream {
default filius_production;
test-filius.hub.mdg-hamburg.de filius_test;
filius.hub.mdg-hamburg.de filius_production;
}
# Own name instead of `$connection_upgrade`: this `conf.d` holds further
# snippets, the openresty already ships `$connection_upgrade`, and two `map`s
# of the same name are a start-up error in nginx.
map $http_upgrade $filius_connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name test-filius.hub.mdg-hamburg.de filius.hub.mdg-hamburg.de;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name test-filius.hub.mdg-hamburg.de filius.hub.mdg-hamburg.de;
http2 on;
# Wildcard for *.hub.mdg-hamburg.de; both names are one label under
# `hub`, as the wildcard requires.
ssl_certificate /etc/ssl/certs/hub.combined;
ssl_certificate_key /etc/ssl/private/hub.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# One file for both environments, without a variable in the path: such a
# path is opened by the worker per request, not by the master at start,
# and the worker may not write in `/var/log/nginx`. Which environment a
# line belongs to is told by each stack's own log (`docker compose logs`).
access_log /var/log/nginx/filius-access.log;
error_log /var/log/nginx/filius-error.log;
# ── The two WebSockets: /sync and /sim ───────────────────────────
#
# `/sync` is the shared document (Hocuspocus), `/sim?project=…` the
# simulation room. Both stay open as long as the project is open.
#
# * `Upgrade`/`Connection`: without them nginx does not pass the
# handshake on.
# * 3600 s: the limit applies between two bytes. F pings every 30 s, so
# an open connection is never silent that long — but a connection
# that is cut after the default 60 s reconnects with a complete
# sign-in and a reload of the document, and everyone sees the others
# blink.
# * `Origin` passes through unchanged (nginx forwards every request
# header); F checks it against `FILIUS_ORIGINS`.
location ~ ^/(sync|sim)$ {
proxy_pass http://$filius_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $filius_connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_buffering off;
proxy_connect_timeout 10s;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# ── Large files: /blobs ──────────────────────────────────────────
#
# Pictures and other binary files from 16 KB on go here as one request
# each, up to 16 MiB (`BLOB_MAX_BYTES` in `packages/server/src/blobs.ts`).
# The limit here is **above** F's on purpose: then F answers a file that
# is too large itself, with `413 tooLarge` and its CORS headers, and the
# editor can say so. nginx's own 413 has no CORS headers; the browser
# hands the editor only "network error", and nobody looks for the cause
# in an nginx default on another machine.
location /blobs {
proxy_pass http://$filius_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
client_max_body_size 20m;
proxy_connect_timeout 10s;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}
# ── Everything else ──────────────────────────────────────────────
#
# The editor's page and bundle, `/__auth` (ticket in, cookie out),
# `/svc/*` (the dashboard's service route, guarded by
# `FILIUS_SERVICE_KEY`; the dashboard reaches F under this name, so if
# this proxy is down, creating and deleting projects is down too) and
# `/healthz`. F limits the bodies of `/svc/*` to 64 KiB itself, so
# nginx's default of 1 MB stays.
location / {
proxy_pass http://$filius_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_connect_timeout 10s;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}
}
# ── When something does not work ──────────────────────────────────────
#
# | Symptom | Cause |
# |---|---|
# | `openresty -t`: host not found in upstream "filius-lxc.invalid…" | The placeholder is still there, see above. |
# | 502 on one host only | That stack is not running or does not publish its port. `curl http://<address>:8081/healthz` (test) or `:8080` (production) from the openresty. |
# | Test host shows production's projects (or the other way round) | The two ports are swapped in the `upstream`s or in the compose files. |
# | Editor opens, but "connection lost" right away | `/sync` or `/sim` gets no `Upgrade`, or `FILIUS_ORIGINS` does not name this host (F logs `sync.foreignOrigin`). |
# | Sign-in in the dashboard's frame fails silently | The stack runs with `FILIUS_INSECURE_COOKIE=1`, or the dashboard of the other environment issued the ticket. |
# | Large picture: "upload failed" without a reason | `client_max_body_size` in `/blobs` is below 16 MiB. |
# | Editor in the dashboard stays a white box | Something sets `X-Frame-Options` for this host. `curl -sI https://filius.hub.mdg-hamburg.de/` |