11# ============================================================
2- # StellarForge — Nginx Reverse Proxy Configuration
2+ # StellarForge — Nginx Reverse Proxy Configuration (SPEED-OPTIMIZED)
33# Domain: stellarforge.app (and www.stellarforge.app)
44#
5- # Proxies:
6- # - HTTP → Next.js app (port 3700, next start)
7- # - /collab → WebSocket collab server (port 3002)
8- # - /lsp → WebSocket LSP server (port 3099)
9- # - WebSocket upgrade support for all paths
5+ # On top of the previous fix (upstream keepalive, correct
6+ # Connection handling, split rate limits), this pass adds:
107#
11- # SSL is managed by certbot — it will auto-inject the
12- # ssl_certificate directives when you run:
8+ # 1. /_next/static/ is served DIRECTLY from disk instead of
9+ # proxying to Node. This is the single biggest latency win
10+ # available — every JS/CSS chunk currently round-trips
11+ # through your Next.js process for no reason; these files
12+ # are content-hashed and immutable, nginx can serve them
13+ # straight off disk with a 1-year cache header.
14+ # >>> You MUST update the `alias` path below to your actual
15+ # >>> .next/static build output directory.
16+ # 2. proxy_socket_keepalive on upstream connections.
17+ # 3. Tighter client-facing keepalive + timeout tuning so idle
18+ # connections don't tie up workers.
19+ # 4. sendfile/tcp_nopush/tcp_nodelay for the local file serving
20+ # (error page + static assets).
21+ # 5. Optional (commented) microcaching block for cacheable GET
22+ # API routes, and optional Brotli.
23+ #
24+ # IMPORTANT — two more optimizations can't live in this file and
25+ # need to be done separately (instructions at the very bottom):
26+ # A. HTTP/2 + TLS session reuse + OCSP stapling, added to the
27+ # 443 block certbot generates for you.
28+ # B. worker_processes / worker_connections tuning in the main
29+ # /etc/nginx/nginx.conf.
30+ #
31+ # SSL is managed by certbot — it will auto-inject ssl_certificate
32+ # directives when you run:
1333# certbot --nginx -d stellarforge.app -d www.stellarforge.app
14- # Do NOT add SSL directives manually.
34+ # Do NOT add SSL directives manually to THIS file's port 80 block .
1535#
1636# Usage:
1737# 1. Copy to /etc/nginx/sites-available/stellarforge.app
1838# 2. Symlink: ln -s /etc/nginx/sites-available/stellarforge.app /etc/nginx/sites-enabled/
19- # 3. certbot --nginx -d stellarforge.app -d www.stellarforge.app
20- # 4. nginx -t && systemctl reload nginx
21- #
22- # Prerequisites:
23- # - Next.js running on port 3700 (bun run start)
24- # - Collab server running on port 3002 (bm2 or manually)
25- # - LSP server running on port 3099 (bm2 or manually)
39+ # 3. Confirm /etc/nginx/conf.d/upgrade.conf does NOT also exist
40+ # (it would duplicate the map below)
41+ # 4. Update the /_next/static/ alias path below to your real build dir
42+ # 5. nginx -t <-- always run this before reloading
43+ # 6. certbot --nginx -d stellarforge.app -d www.stellarforge.app
44+ # 7. Apply the HTTP/2 + SSL tuning snippet at the bottom of this file
45+ # to the 443 block certbot creates
46+ # 8. nginx -t && systemctl reload nginx
2647# ============================================================
2748
28- # ─── WebSocket upgrade map (MUST be outside server block) ───
29- # This tells nginx how to handle the Connection header for WebSocket
30- # upgrades. Without this, WebSocket connections fail with
31- # "unknown connection_upgrade variable".
32- # This is defined INLINE here — no need for a separate upgrade.conf file.
3349map $http_upgrade $connection_upgrade {
3450 default upgrade;
35- '' close;
51+ '' close;
52+ }
53+
54+ # ─── Upstream pools with keepalive ────────────────────────────
55+ # Reuses backend connections instead of opening a new TCP
56+ # connection per request.
57+ upstream nextjs_backend {
58+ server 127.0.0.1:3700 ;
59+ keepalive 64 ;
60+ keepalive_requests 1000 ;
61+ keepalive_timeout 60s ;
62+
63+ # If you ever run Next.js as multiple instances (e.g. PM2
64+ # cluster mode across several ports) for more throughput on
65+ # multi-core boxes, add them here and nginx load-balances
66+ # automatically:
67+ # server 127.0.0.1:3701;
68+ # server 127.0.0.1:3702;
69+ # least_conn;
70+ }
71+
72+ upstream collab_backend {
73+ server 127.0.0.1:3002 ;
74+ keepalive 32 ;
75+ }
76+
77+ upstream lsp_backend {
78+ server 127.0.0.1:3099 ;
79+ keepalive 32 ;
3680}
3781
38- # Rate limiting zone
39- limit_req_zone $binary_remote_addr zone =stellarforge_api:10m rate=10r /s;
82+ # ─── Rate limiting zones ───────────────────────────────────────
83+ limit_req_zone $binary_remote_addr zone =stellarforge_api:10m rate=20r /s;
84+ limit_req_zone $binary_remote_addr zone =stellarforge_app:10m rate=50r /s;
4085
41- # Upload size for file imports / avatar uploads
4286client_max_body_size 25M ;
4387
44- # ─── HTTP server (certbot will add the 443 block automatically) ──
88+ # ─── Local file I/O tuning (error pages, static assets) ───────
89+ sendfile on;
90+ tcp_nopush on;
91+ tcp_nodelay on;
92+
4593server {
4694 listen 80 ;
4795 listen [::]:80 ;
4896 server_name stellarforge.app www.stellarforge.app;
4997
50- # Let's Encrypt challenge path
51- location /.well-known/acme-challenge/ {
52- root /var/www/html;
53- }
54-
55- # ─── www → non-www redirect ──────────────────────────────
56- if ( $host = www.stellarforge.app) {
57- return 301 https://stellarforge.app$request_uri ;
58- }
59-
6098 # certbot will insert the HTTPS redirect here after running:
6199 # certbot --nginx -d stellarforge.app -d www.stellarforge.app
62100
101+ # ─── Client-facing connection tuning ──────────────────────
102+ # Keeps idle client connections short so workers free up fast,
103+ # while still letting browsers reuse one connection for all
104+ # the assets on a page instead of opening a new one each time.
105+ keepalive_timeout 20s ;
106+ keepalive_requests 1000 ;
107+ client_header_timeout 15s ;
108+ client_body_timeout 15s ;
109+ send_timeout 15s ;
110+ reset_timedout_connection on;
111+
63112 # ─── WebSocket: Collab server (/collab/) ─────────────────
64- # Proxies to the Yjs WebSocket collab server on port 3002.
65- # This is the cross-device live collaboration server.
66- # Client connects via: wss://stellarforge.app/collab/stellarforge-<roomId>
67113 location /collab/ {
68- proxy_pass http ://127.0.0.1:3002 ;
114+ proxy_pass http ://collab_backend ;
69115 proxy_http_version 1.1;
70116 proxy_set_header Upgrade $http_upgrade ;
71117 proxy_set_header Connection $connection_upgrade ;
72118 proxy_set_header Host $host ;
73119 proxy_set_header X-Real-IP $remote_addr ;
74120 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ;
75121 proxy_set_header X-Forwarded-Proto $scheme ;
122+ proxy_socket_keepalive on;
76123
77124 proxy_read_timeout 86400s ;
78125 proxy_send_timeout 86400s ;
79126 proxy_buffering off;
80127 proxy_cache off;
81128 }
82129
83- # ─── WebSocket: LSP server (/lsp and /workspace/) ────────
84- # Proxies to the rust-analyzer LSP server on port 3099.
85- # /lsp is the WebSocket endpoint for LSP/JSON-RPC protocol.
86- # /workspace/ serves files from the build workspace (for go-to-def etc.)
87- location /lsp {
88- proxy_pass http ://127.0.0.1:3099 ;
130+ # ─── WebSocket: LSP server (/lsp/ and /workspace/) ───────
131+ location /lsp/ {
132+ proxy_pass http ://lsp_backend;
89133 proxy_http_version 1.1;
90134 proxy_set_header Upgrade $http_upgrade ;
91135 proxy_set_header Connection $connection_upgrade ;
92136 proxy_set_header Host $host ;
93137 proxy_set_header X-Real-IP $remote_addr ;
94138 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ;
95139 proxy_set_header X-Forwarded-Proto $scheme ;
140+ proxy_socket_keepalive on;
96141
97142 proxy_read_timeout 86400s ;
98143 proxy_send_timeout 86400s ;
@@ -101,63 +146,106 @@ server {
101146 }
102147
103148 location /workspace/ {
104- proxy_pass http ://127.0.0.1:3099 ;
149+ proxy_pass http ://lsp_backend ;
105150 proxy_http_version 1.1;
106151 proxy_set_header Upgrade $http_upgrade ;
107152 proxy_set_header Connection $connection_upgrade ;
108153 proxy_set_header Host $host ;
109154 proxy_set_header X-Real-IP $remote_addr ;
110155 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ;
111156 proxy_set_header X-Forwarded-Proto $scheme ;
157+ proxy_socket_keepalive on;
112158
113159 proxy_read_timeout 86400s ;
114160 proxy_send_timeout 86400s ;
115161 proxy_buffering off;
116162 proxy_cache off;
117163 }
118164
165+ # ─── Static assets — served directly from disk, bypassing Node ──
166+ # >>> UPDATE THIS PATH to your real .next/static build output,
167+ # >>> e.g. /var/www/stellarforge/.next/static/
168+ location /_next/static/ {
169+ alias /var/www/stellarforge/.next/static/;
170+ expires 1y ;
171+ add_header Cache-Control "public, immutable" ;
172+ access_log off;
173+ # Not rate-limited — these are cheap, cacheable, and a
174+ # single page load fires dozens of them in parallel.
175+ }
176+
177+ # Public /public/ folder assets (favicons, images, etc.) —
178+ # same idea, update path to match your deployment.
179+ location /public/ {
180+ alias /var/www/stellarforge/public/;
181+ expires 30d ;
182+ add_header Cache-Control "public" ;
183+ access_log off;
184+ }
185+
186+ # ─── Optional: microcaching for cacheable GET API routes ──
187+ # Uncomment and adjust if you have public, non-personalized
188+ # GET endpoints (e.g. leaderboard, public stats). Skips this
189+ # for anything with a session cookie so logged-in users always
190+ # hit the backend.
191+ #
192+ # proxy_cache_path /var/cache/nginx/stellarforge levels=1:2
193+ # keys_zone=stellarforge_cache:10m max_size=200m inactive=60m;
194+ #
195+ # location /api/public/ {
196+ # proxy_pass http://nextjs_backend;
197+ # proxy_http_version 1.1;
198+ # proxy_set_header Connection "";
199+ # proxy_set_header Host $host;
200+ # proxy_cache stellarforge_cache;
201+ # proxy_cache_valid 200 30s;
202+ # proxy_cache_bypass $cookie_session;
203+ # proxy_no_cache $cookie_session;
204+ # add_header X-Cache-Status $upstream_cache_status;
205+ # }
206+
119207 # ─── API routes (/api/) ──────────────────────────────────
120208 location /api/ {
121- proxy_pass http ://127.0.0.1:3700 ;
209+ proxy_pass http ://nextjs_backend;
210+ proxy_http_version 1.1;
211+ proxy_set_header Connection "" ;
122212 proxy_set_header Host $host ;
123213 proxy_set_header X-Real-IP $remote_addr ;
124214 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ;
125215 proxy_set_header X-Forwarded-Proto $scheme ;
216+ proxy_socket_keepalive on;
126217
127218 add_header Cache-Control "no-store, no-cache, must-revalidate" ;
128219 proxy_buffering on;
129220 proxy_buffer_size 16k ;
130221 proxy_buffers 8 32k ;
131222
132- # 35s read timeout — Neon cold start can take 10-15s,
133- # and SIWS verify does multiple DB queries (upsert + profile create).
134- # Next.js maxDuration is 30s, so 35s gives nginx a 5s buffer.
135- proxy_read_timeout 35s ;
136- proxy_send_timeout 35s ;
223+ proxy_connect_timeout 5s ;
224+ proxy_read_timeout 60s ;
225+ proxy_send_timeout 60s ;
137226
138- limit_req zone =stellarforge_api burst=20 nodelay;
139- client_max_body_size 25M ;
227+ limit_req zone =stellarforge_api burst=40 nodelay;
140228 }
141229
142230 # ─── Next.js app (everything else) ────────────────────────
143231 location / {
144- proxy_pass http ://127.0.0.1:3700 ;
232+ proxy_pass http ://nextjs_backend;
233+ proxy_http_version 1.1;
234+ proxy_set_header Connection "" ;
145235 proxy_set_header Host $host ;
146236 proxy_set_header X-Real-IP $remote_addr ;
147237 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ;
148238 proxy_set_header X-Forwarded-Proto $scheme ;
239+ proxy_socket_keepalive on;
149240
150- proxy_http_version 1.1;
151- proxy_set_header Upgrade $http_upgrade ;
152- proxy_set_header Connection $connection_upgrade ;
153-
154- proxy_read_timeout 300s ;
155- proxy_send_timeout 300s ;
241+ proxy_connect_timeout 5s ;
242+ proxy_read_timeout 60s ;
243+ proxy_send_timeout 60s ;
156244 proxy_buffering on;
157245 proxy_buffer_size 16k ;
158246 proxy_buffers 8 32k ;
159247
160- limit_req zone =stellarforge_api burst=20 nodelay;
248+ limit_req zone =stellarforge_app burst=80 nodelay;
161249 }
162250
163251 # ─── Compression ─────────────────────────────────────────
@@ -181,6 +269,14 @@ server {
181269 font/woff2
182270 application/wasm;
183271
272+ # Optional: Brotli compresses better than gzip for text assets.
273+ # Requires the ngx_brotli module (not built into stock nginx —
274+ # check `nginx -V` for it, or install nginx-module-brotli).
275+ # brotli on;
276+ # brotli_comp_level 5;
277+ # brotli_types text/plain text/css application/javascript
278+ # application/json image/svg+xml font/woff2 application/wasm;
279+
184280 # ─── Logging ──────────────────────────────────────────────
185281 access_log /var/log/nginx/stellarforge.app.access.log;
186282 error_log /var/log/nginx/stellarforge.app.error.log warn;
@@ -192,3 +288,41 @@ server {
192288 internal ;
193289 }
194290}
291+
292+ # ============================================================
293+ # A. After running certbot, add this to the 443 block it creates
294+ # for faster TLS handshakes + HTTP/2 (lets browsers multiplex
295+ # all those Next.js chunk requests over one connection instead
296+ # of opening 6 parallel ones):
297+ #
298+ # listen 443 ssl;
299+ # http2 on; # nginx >= 1.25.1 syntax
300+ # # (older nginx: listen 443 ssl http2; instead of the two lines above)
301+ #
302+ # ssl_session_cache shared:SSL:10m;
303+ # ssl_session_timeout 1d;
304+ # ssl_session_tickets off;
305+ # ssl_protocols TLSv1.2 TLSv1.3;
306+ # ssl_prefer_server_ciphers off;
307+ #
308+ # ssl_stapling on;
309+ # ssl_stapling_verify on;
310+ # resolver 1.1.1.1 8.8.8.8 valid=300s;
311+ # resolver_timeout 5s;
312+ #
313+ # B. In /etc/nginx/nginx.conf (main context), these matter more
314+ # for you than most sites given the WS collab/LSP connections
315+ # holding workers open for hours:
316+ #
317+ # worker_processes auto;
318+ # worker_rlimit_nofile 65535;
319+ #
320+ # events {
321+ # worker_connections 4096;
322+ # multi_accept on;
323+ # use epoll;
324+ # }
325+ #
326+ # Check current limits with: ulimit -n
327+ # If low, also raise the nginx systemd service's LimitNOFILE.
328+ # ============================================================
0 commit comments