Files
EpicNext-Cms/deployment/proxy/nginx.conf
T
openhands f0dcf440a7 fix(proxy): split rate limiting into page and static zones
The single server-scope limit_req (30r/s) treated a page load and a room
load as the same thing. Loading a Nitro room fires several hundred gamedata
icons in one burst, which that zone answered with 503s, so icons showed up
late in the client.

Add a separate static zone (1000r/s, burst 1000, nodelay) for the gamedata
and client asset locations, and apply the page-rate zone explicitly on the
main route instead of at server scope. Connection limit stays server-wide.

Measured: 900 icon requests in burst now all return 200, while 200 parallel
requests on / are still rejected.
2026-10-01 17:49:41 +02:00

73 lines
2.9 KiB
Nginx Configuration File

# Canonical nginx config for the EpicNabbo CMS edge.
# Source of truth: repository deployment/proxy/nginx-cms.conf (the site block)
# and this file. Installed/synced by scripts/nginx-sync.sh so it cannot be
# lost again while nginx keeps running on an in-memory copy.
#
# Traffic path: Cloudflare -> Traefik (:443) -> nginx (:9443) -> CMS (:3002),
# with direct Cloudflare-origin and game-client (ws.epicnabbo.nl) connections
# also terminating on :9443.
# nginx is the last layer that can still rewrite Cache-Control, so it owns the
# headers it adds explicitly; everything proxied to the CMS is passed through
# untouched unless this file says otherwise.
user www-data;
worker_processes auto;
# Raise the file-descriptor rlimit for the workers. Must stay <= the master's
# RLIMIT_NOFILE *hard* limit, otherwise nginx refuses to start with
# "setrlimit(RLIMIT_NOFILE) failed". Bounded from above by the systemd drop-in
# /etc/systemd/system/nginx.service.d/override.conf (LimitNOFILE=65536).
worker_rlimit_nofile 65536;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log warn;
events {
worker_connections 2048;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Compression is done once, at the edge (Traefik / Cloudflare). Enabling
# gzip here too would double-compress proxied responses and fight Vary.
gzip off;
sendfile on;
tcp_nopush on;
server_tokens off;
keepalive_timeout 30s;
client_max_body_size 64m;
client_body_buffer_size 16k;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
# Rate limiting per client IP.
#
# Two zones, because a room load and a page load are not the same thing.
# Loading a Nitro room fires several hundred gamedata icons in one burst;
# at the page rate that produced 503s on real players. Static assets
# therefore get their own, much higher allowance. These are small immutable
# files, so a request rate is not what protects them anyway — nginx already
# serves them with must-revalidate, and the CMS upstream stays behind
# cms_req_per_ip for the expensive routes.
limit_req_zone $binary_remote_addr zone=cms_req_per_ip:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=cms_static_per_ip:10m rate=1000r/s;
limit_conn_zone $binary_remote_addr zone=cms_conn_per_ip:10m;
# Blue/green cutover: ci-deploy.sh writes the active upstream here, and
# `proxy_pass http://cms_app` below follows it via graceful nginx -s reload.
upstream cms_app {
include /etc/nginx/snippets/cms_upstream_servers.conf;
}
# Cache policy maps and server blocks live in the site file so they are
# synced together and can never drift apart.
include /etc/nginx/sites-enabled/*.conf;
# Trusted edge / real-IP handling (regenerated by scripts/cf-ips-sync.sh
# from the live Cloudflare ranges; installed via scripts/nginx-sync.sh).
include /etc/nginx/conf.d/cloudflare-ips.conf;
}