morphit/ops/nginx/indexer.conf

196 lines
8.3 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================================
# 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;
}
}