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:
@@ -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/` |
|
||||
Reference in New Issue
Block a user