196 lines
8.3 KiB
Text
196 lines
8.3 KiB
Text
# ============================================================================
|
||
# OPTIONAL — ADVANCED "split-subdomain" TOPOLOGY ONLY.
|
||
#
|
||
# The RECOMMENDED deployment is single-host (everything under one hostname);
|
||
# it does NOT use this file. The frontend already proxies /v1/ + /rss/ to the
|
||
# loopback indexer via ops/nginx/web.conf — see docs/RUN-A-MORPHIT-NODE.md §9.
|
||
#
|
||
# Use this file ONLY if you deliberately run the indexer on its own subdomain
|
||
# (e.g. indexer.example.com). That requires ALL of:
|
||
# 1. A separate DNS record + TLS cert for the indexer subdomain.
|
||
# 2. Rebuilding the frontend with MORPHIT_INDEXER_ORIGIN set to that
|
||
# absolute URL (apps/web/src/lib/net/config.ts) — otherwise the
|
||
# same-origin default frontend will keep calling /v1/ on its OWN host
|
||
# and never reach this subdomain.
|
||
# 3. CORS on this server allowing your frontend origin, AND a /rss/ proxy
|
||
# on the FRONTEND host anyway (the footer/<head> RSS links are
|
||
# same-origin), so split buys you little for RSS.
|
||
# If you're not sure, you want the single-host setup — delete any
|
||
# indexer.* DNS record and use ops/nginx/web.conf instead.
|
||
# ============================================================================
|
||
# Morphit indexer — nginx server block.
|
||
#
|
||
# The indexer listens on 127.0.0.1:8081 by default
|
||
# (MORPHIT_INDEXER_LISTEN_PORT, see apps/indexer/src/config/index.ts).
|
||
# Note this is distinct from the relay's default of 8080, so both
|
||
# services can coexist on the same host without port collision.
|
||
# nginx is the only thing that reaches the indexer in production.
|
||
|
||
upstream morphit_indexer {
|
||
server 127.0.0.1:8081;
|
||
keepalive 32;
|
||
}
|
||
|
||
# Per-IP limits — slightly above the indexer's internal tiers so
|
||
# nginx catches abusive traffic before it reaches the Node process.
|
||
limit_req_zone $binary_remote_addr zone=indexer_list:10m rate=150r/m;
|
||
limit_req_zone $binary_remote_addr zone=indexer_resource:10m rate=700r/m;
|
||
|
||
server {
|
||
listen 443 ssl http2;
|
||
listen [::]:443 ssl http2;
|
||
server_name indexer.morphit.io;
|
||
|
||
# ─── TLS ──────────────────────────────────────────────────
|
||
ssl_certificate /etc/letsencrypt/live/indexer.morphit.io/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/indexer.morphit.io/privkey.pem;
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
ssl_prefer_server_ciphers off;
|
||
ssl_session_cache shared:SSL:10m;
|
||
ssl_session_timeout 1d;
|
||
ssl_session_tickets off;
|
||
|
||
# ─── Shared security headers ──────────────────────────────
|
||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||
add_header X-Content-Type-Options "nosniff" always;
|
||
add_header Referrer-Policy "no-referrer" always;
|
||
add_header X-Frame-Options "DENY" always;
|
||
# The indexer is JSON-only — no HTML, no scripts, no styles.
|
||
# `default-src 'none'` makes sure that any accidental HTML
|
||
# response (e.g. a misconfigured error page) can't load
|
||
# anything at all.
|
||
add_header Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none'" always;
|
||
|
||
# ─── Body cap at the edge ─────────────────────────────────
|
||
# Indexer is read-only (no POST/PUT/PATCH/DELETE endpoints in
|
||
# apps/indexer/src/api). The 4 KiB cap matches
|
||
# MORPHIT_INDEXER_MAX_BODY_BYTES (default 4096) so nginx
|
||
# rejects oversized requests at the edge before they reach
|
||
# the Node process. GET requests with absurdly long
|
||
# querystrings are also bounded by nginx's default
|
||
# large_client_header_buffers.
|
||
client_max_body_size 4k;
|
||
|
||
# ─── Upstream connection settings ─────────────────────────
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Connection "";
|
||
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 $scheme;
|
||
proxy_set_header Host $host;
|
||
# Operator-only per-source price-feed health (morphit-ops 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. Set at server scope so the inheriting `/v1/` location (which
|
||
# defines no proxy_set_header of its own) picks it up too.
|
||
proxy_set_header X-Morphit-Local-Health "";
|
||
|
||
proxy_connect_timeout 2s;
|
||
proxy_send_timeout 10s;
|
||
proxy_read_timeout 10s;
|
||
|
||
# ─── Compression (cp165 byte budget) ──────────────────────
|
||
# Every API response gets gzip-compressed. JSON compresses
|
||
# ~4-8× — a 200 KB orderbook listing becomes ~30 KB on the
|
||
# wire. This is the single biggest win for Sally on a slow
|
||
# mobile connection AND for third-party API consumers
|
||
# building bots / explorers on top of Morphit. Costs CPU on
|
||
# the box (small — ~1ms per response at level 5) and is the
|
||
# standard tradeoff every HTTP API makes.
|
||
gzip on;
|
||
gzip_vary on;
|
||
gzip_proxied any;
|
||
gzip_comp_level 5;
|
||
gzip_min_length 256;
|
||
gzip_types
|
||
application/json
|
||
application/javascript
|
||
application/xml
|
||
application/atom+xml
|
||
application/rss+xml
|
||
text/plain
|
||
text/css
|
||
text/javascript
|
||
text/xml;
|
||
# Brotli would compress ~15-20% smaller still but requires
|
||
# the brotli module (ngx_brotli). Operators can opt in by
|
||
# uncommenting after installing the module; the gzip above
|
||
# remains as the fallback for clients that don't accept br.
|
||
# brotli on;
|
||
# brotli_comp_level 5;
|
||
# brotli_types
|
||
# application/json
|
||
# application/javascript
|
||
# text/plain
|
||
# text/css;
|
||
|
||
# ─── Routes ───────────────────────────────────────────────
|
||
|
||
# Health — unthrottled, no cache. Monitoring systems hit this
|
||
# frequently; rate-limiting it would cause false alarms.
|
||
location = /v1/health {
|
||
proxy_pass http://morphit_indexer;
|
||
add_header Cache-Control "no-store" always;
|
||
}
|
||
|
||
# SSE streams (orderbook / chat / instances live updates).
|
||
# Long-lived connections, so: disable proxy buffering so events
|
||
# flush to the client immediately, disable caching, and raise the
|
||
# upstream read timeout well above the indexer's heartbeat-comment
|
||
# interval so nginx doesn't sever an idle-but-healthy stream (the
|
||
# default 10s below would cut it between heartbeats). MUST precede
|
||
# the /v1/(orderbook|orders|chat|accounts) regex so the *.../stream
|
||
# paths match here first (nginx uses the first matching regex).
|
||
location ~ ^/v1/.*/stream$ {
|
||
proxy_pass http://morphit_indexer;
|
||
proxy_buffering off;
|
||
proxy_cache off;
|
||
proxy_read_timeout 1h;
|
||
add_header Cache-Control "no-store" always;
|
||
}
|
||
|
||
# List endpoints — the busier tier.
|
||
location ~ ^/v1/(orderbook|orders|chat|accounts) {
|
||
limit_req zone=indexer_list burst=30 nodelay;
|
||
limit_req_status 429;
|
||
proxy_pass http://morphit_indexer;
|
||
}
|
||
|
||
# Resource endpoints — lighter, single-row lookups.
|
||
location ~ ^/v1/(profiles|release) {
|
||
limit_req zone=indexer_resource burst=60 nodelay;
|
||
limit_req_status 429;
|
||
proxy_pass http://morphit_indexer;
|
||
}
|
||
|
||
# Catch-all: any other /v1/* path returns 404 from the indexer
|
||
# with the ErrorResponse shape.
|
||
location /v1/ {
|
||
proxy_pass http://morphit_indexer;
|
||
}
|
||
|
||
# RSS/Atom feeds (orderbook). Public, cacheable, list-tier
|
||
# traffic. The indexer mounts these under /rss/* (see
|
||
# apps/indexer/src/api/rssOrderbook.ts); application/rss+xml is in
|
||
# the gzip_types above so feeds compress on the wire.
|
||
location /rss/ {
|
||
limit_req zone=indexer_list burst=30 nodelay;
|
||
limit_req_status 429;
|
||
proxy_pass http://morphit_indexer;
|
||
}
|
||
|
||
# Everything else → 404. Avoids exposing nginx's default pages.
|
||
location / {
|
||
return 404;
|
||
}
|
||
}
|
||
|
||
# HTTP → HTTPS redirect.
|
||
server {
|
||
listen 80;
|
||
listen [::]:80;
|
||
server_name indexer.morphit.io;
|
||
location / {
|
||
return 301 https://$host$request_uri;
|
||
}
|
||
}
|