189 lines
9.2 KiB
Nginx Configuration File
189 lines
9.2 KiB
Nginx Configuration File
# Morphit frontend nginx — serves the built SvelteKit static site and
|
|
# reverse-proxies the API paths to the relay + indexer running on the
|
|
# HOST. Runs BEHIND BunkerWeb inside the bunkerweb_net Docker network,
|
|
# on plain :80.
|
|
#
|
|
# BunkerWeb (see ../docker-compose.yml + ../bunkerweb.env.example) is
|
|
# the public entry point: it terminates TLS, runs the OWASP-CRS WAF,
|
|
# rate-limits, sets the real client IP in X-Forwarded-For, and adds the
|
|
# security headers (HSTS/CSP/X-Frame-Options/...). It then proxies
|
|
# EVERY path to this container (REVERSE_PROXY_HOST=http://frontend:80).
|
|
# So this config intentionally does none of that — no TLS, no security
|
|
# headers. Its only jobs are static-file serving + path routing.
|
|
#
|
|
# This mirrors the routing in ops/nginx/web.conf (the bare-metal
|
|
# single-host vhost) MINUS the TLS + security-header blocks that
|
|
# BunkerWeb owns here. Keep the two in sync when either changes.
|
|
#
|
|
# UPSTREAMS — host.docker.internal resolves to the host via
|
|
# `extra_hosts: ["host.docker.internal:host-gateway"]` in
|
|
# docker-compose.yml, so the relay + indexer stay on the host (NOT in a
|
|
# container). Ports are the morphit defaults: relay 8080, indexer 8081
|
|
# (MORPHIT_RELAY_LISTEN_PORT / MORPHIT_INDEXER_LISTEN_PORT). Change
|
|
# them here if you bind the services to non-default ports.
|
|
#
|
|
# IMPORTANT — the relay + indexer must listen on an address the
|
|
# Docker bridge can reach (NOT 127.0.0.1 only): bind them to the
|
|
# host-gateway address or 0.0.0.0 and firewall those ports so only
|
|
# the bridge can reach them (the UFW rules in
|
|
# ops/ansible/roles/hardening do this). A loopback-only bind is
|
|
# unreachable from this container and the proxy_pass returns 502.
|
|
#
|
|
# REAL CLIENT IP — BunkerWeb already put the real client as the LEFTMOST
|
|
# X-Forwarded-For entry. These blocks APPEND to the chain via
|
|
# $proxy_add_x_forwarded_for (so XFF becomes "client, bunkerweb-ip"),
|
|
# which keeps the client leftmost — the relay reads XFF[0]. Do NOT
|
|
# prepend or set X-Real-IP to $remote_addr here: $remote_addr is
|
|
# BunkerWeb's container IP, not the client, and overwriting would break
|
|
# the per-IP signup rate limiting. The relay's
|
|
# MORPHIT_RELAY_TRUSTED_PROXY_IPS must cover this Docker network's CIDR
|
|
# (172.20.0.0/16) — that single CIDR covers BOTH the BunkerWeb and the
|
|
# frontend containers, since the relay sees this container as the
|
|
# immediate peer.
|
|
|
|
server {
|
|
listen 80;
|
|
server_name _;
|
|
|
|
root /usr/share/nginx/html;
|
|
index index.html;
|
|
|
|
# Tell browsers text responses are UTF-8. Without this, a plain-text
|
|
# file like /canary.txt (which contains em-dashes, bullets, box-
|
|
# drawing) is served with no charset, so browsers fall back to
|
|
# windows-1252 and render mojibake (─ → "─", — → "—"). nginx's
|
|
# default charset_types already covers text/plain, text/html, etc.,
|
|
# so this one line fixes every text asset. (cp432)
|
|
charset utf-8;
|
|
|
|
# Serve the SvelteKit build's pre-compressed .gz siblings when the
|
|
# client accepts gzip (zero CPU — the compression happened at build
|
|
# time). BunkerWeb still does the client-facing gzip/brotli at the
|
|
# edge, so this only saves the frontend->BunkerWeb hop, but it's
|
|
# free. A plain `gzip on` covers proxied JSON responses too.
|
|
gzip on;
|
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
|
gzip_static on;
|
|
|
|
# Never expose dotfiles / editor leftovers if one ever lands in the
|
|
# build dir. 404 (not 403) gives no signal about whether the path
|
|
# exists. Mirrors ops/nginx/web.conf.
|
|
location ~ /\.(?!well-known) {
|
|
return 404;
|
|
}
|
|
location ~* \.(env|git|sql|bak|old|orig|swp|tmp)$ {
|
|
return 404;
|
|
}
|
|
|
|
# ─── Relay — fund-spending endpoints ──────────────────────────
|
|
# The frontend calls same-origin /relay/v1/* (e.g.
|
|
# /relay/v1/account/create); strip the /relay prefix so the relay
|
|
# process sees bare /v1/* (mirrors web.conf's
|
|
# `rewrite ^/relay/(.*)$ /$1`). The longer /relay/ prefix means
|
|
# /relay/v1/* never falls into the /v1/ block below.
|
|
location /relay/ {
|
|
rewrite ^/relay/(.*)$ /$1 break;
|
|
proxy_pass http://host.docker.internal:8080;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Connection "";
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto https;
|
|
client_max_body_size 64k;
|
|
}
|
|
|
|
# ─── Indexer — read-only public API (/v1/*), served as-is ─────
|
|
location /v1/ {
|
|
proxy_pass http://host.docker.internal:8081;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Connection "";
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto https;
|
|
# Operator-only per-source price-feed health is gated on
|
|
# X-Morphit-Local-Health (sent by the local ops-cli over the
|
|
# bridge). Clear any client-supplied value so a public caller
|
|
# can never forge it and read the per-source block.
|
|
proxy_set_header X-Morphit-Local-Health "";
|
|
client_max_body_size 4k;
|
|
}
|
|
|
|
# ─── SSE live-update streams (orderbook / chat / instances) ───
|
|
# The indexer also sends `X-Accel-Buffering: no` on every stream,
|
|
# which every nginx hop (this container + BunkerWeb) honors, so
|
|
# events arrive live through both proxies. This block is the
|
|
# belt-and-suspenders version: buffering off + a long read timeout.
|
|
# A regex location beats the /v1/ prefix, so .../stream paths land
|
|
# here ahead of the plain /v1/ block above.
|
|
location ~ ^/v1/.*/stream$ {
|
|
proxy_pass http://host.docker.internal:8081;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Connection "";
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto https;
|
|
proxy_buffering off;
|
|
proxy_read_timeout 1h;
|
|
}
|
|
|
|
# ─── Indexer RSS/Atom orderbook feeds (/rss/*) ────────────────
|
|
location /rss/ {
|
|
proxy_pass http://host.docker.internal:8081;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Connection "";
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto https;
|
|
}
|
|
|
|
# ─── MCP — read-only AI-agent orderbook surface (/mcp) ────────
|
|
# The host MCP runs the HTTP transport on port 8124. It is NOT
|
|
# virtual-hosted; it uses the Host header only for DNS-rebinding
|
|
# defense, whose default allowlist includes the loopback form — so
|
|
# present `Host: 127.0.0.1:8124` upstream (forwarding $host would be
|
|
# rejected 403). Responses can be long-running for federation
|
|
# queries, hence the longer read timeout.
|
|
location /mcp {
|
|
proxy_pass http://host.docker.internal:8124;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Connection "";
|
|
proxy_set_header Host 127.0.0.1:8124;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto https;
|
|
client_max_body_size 256k;
|
|
proxy_read_timeout 120s;
|
|
}
|
|
|
|
# ─── Update surface — never cache ─────────────────────────────
|
|
# /service-worker.js drives update detection: the browser refetches it
|
|
# on every update check (the app registers the worker with
|
|
# updateViaCache:'none'), and a byte-changed worker is what raises the
|
|
# in-app "Load it now" snackbar after a deploy. verify.json carries
|
|
# this build's version + asset-hash manifest, read by the app's
|
|
# update-version poll and the "About this instance" auto-verify. If
|
|
# EITHER is served stale — e.g. from BunkerWeb's own edge cache, or any
|
|
# CDN in front — the client never learns a new build is live and the
|
|
# update prompt never appears. (This is the "no update snackbar on
|
|
# mobile OR PC" bug: ops/nginx/web.conf carried this no-cache block but
|
|
# THIS file did not, and the two configs drifted out of sync.) Mark
|
|
# both no-cache so BunkerWeb and the browser always revalidate. No
|
|
# security headers here — BunkerWeb owns those at the edge; this server
|
|
# block sets none to inherit, so the add_header reset footgun that
|
|
# web.conf has to work around does not apply here.
|
|
location = /service-worker.js {
|
|
add_header Cache-Control "no-cache" always;
|
|
try_files $uri =404;
|
|
}
|
|
location = /verify.json {
|
|
add_header Cache-Control "no-cache" always;
|
|
try_files $uri =404;
|
|
}
|
|
|
|
# ─── SvelteKit SPA fallback ───────────────────────────────────
|
|
# Longest-prefix match means the proxy blocks above win for their
|
|
# paths; every other request falls here and returns the SPA entry
|
|
# so client-side routing handles the URL.
|
|
location / {
|
|
try_files $uri $uri.html $uri/index.html /index.html;
|
|
}
|
|
}
|