# Cyclone frontend — nginx site config. # # Two responsibilities: # 1. Serve the built SPA bundle from /usr/share/nginx/html with a # fallback to index.html for client-side routing. # 2. Reverse-proxy /api/* to the backend service so the SPA can call # relative URLs (VITE_API_BASE_URL empty) without CORS round-trips. # # `backend` resolves via Docker's user-defined network (compose service # name). `chunked_transfer_encoding off` keeps SSE/NDJSON streams flowing # instead of being buffered by nginx — uvicorn already streams, but the # explicit override makes the intent obvious and survives future nginx # default changes. upstream cyclone_backend { server backend:8000; keepalive 16; } server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # Reasonable defaults for a single-operator dashboard. client_max_body_size 25m; # gzip JS/CSS/JSON/SVG so the initial bundle loads faster. gzip on; gzip_comp_level 5; gzip_min_length 256; gzip_proxied any; gzip_vary on; gzip_types application/javascript application/json application/xml image/svg+xml text/css text/plain; # Cache fingerprinted assets aggressively; index.html stays fresh. location ~* \.(?:js|css|woff2?|ttf|otf|eot|svg|png|jpg|jpeg|webp|ico)$ { try_files $uri =404; expires 7d; add_header Cache-Control "public, immutable"; } # /api/* → backend. Don't buffer so live-tail NDJSON streams flow. location /api/ { proxy_pass http://cyclone_backend; proxy_http_version 1.1; proxy_set_header Host $host; 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 Connection ""; # Keep idle NDJSON streams alive across nginx's default 60s. proxy_read_timeout 1h; proxy_send_timeout 60s; proxy_buffering off; proxy_cache off; chunked_transfer_encoding on; add_header X-Accel-Buffering no; } # SPA fallback — anything that isn't a real file in / serves index.html # so client-side routes (/inbox, /claims/:id) don't 404 on refresh. location / { try_files $uri $uri/ /index.html; } # Lightweight health probe for orchestrators that prefer /healthz over # the static index. location = /healthz { access_log off; add_header Content-Type text/plain; return 200 "ok\n"; } }