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.
73 lines
2.9 KiB
Nginx Configuration File
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;
|
|
} |