# Rendered by deploy/bin/exo-render-nginx from deploy/apps.conf. # Placeholders: {{APP}} {{DOMAIN}} {{PORT}} # # Layout: nginx serves the built SPA straight off disk and proxies only /api/*. # A restarting or crashed Node process therefore takes down the form, not the page. limit_req_zone $binary_remote_addr zone=leads_{{APP}}:10m rate=10r/m; # --- ACME challenge + http -> https --------------------------------------- server { listen 80; listen [::]:80; server_name {{DOMAIN}} www.{{DOMAIN}}; # Must come before any try_files fallback, or certbot's challenge 404s. location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://{{DOMAIN}}$request_uri; } } # --- www -> apex ----------------------------------------------------------- server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name www.{{DOMAIN}}; ssl_certificate /etc/letsencrypt/live/{{DOMAIN}}/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/{{DOMAIN}}/privkey.pem; include /etc/nginx/snippets/ssl-params.conf; return 301 https://{{DOMAIN}}$request_uri; } # --- the site -------------------------------------------------------------- server { listen 443 ssl; listen [::]:443 ssl; http2 on; server_name {{DOMAIN}}; ssl_certificate /etc/letsencrypt/live/{{DOMAIN}}/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/{{DOMAIN}}/privkey.pem; include /etc/nginx/snippets/ssl-params.conf; root /srv/exo/{{APP}}/current/dist/client; index index.html; include /etc/nginx/snippets/security-headers.conf; # Vite fingerprints everything under /assets — cache for a year. location ^~ /assets/ { access_log off; # Re-included: this location's own add_header would drop the inherited ones. include /etc/nginx/snippets/security-headers.conf; add_header Cache-Control "public, max-age=31536000, immutable" always; try_files $uri =404; } # The one file that must never be cached, or a deploy goes unseen. # `/` lands here via try_files, so this is where the security headers # actually have to be re-stated (see snippets/security-headers.conf). location = /index.html { include /etc/nginx/snippets/security-headers.conf; add_header Cache-Control "no-cache" always; } location = /robots.txt { access_log off; } location = /sitemap.xml { access_log off; } location = /api/health { access_log off; proxy_pass http://127.0.0.1:{{PORT}}; include /etc/nginx/snippets/proxy-params.conf; } # Second layer of rate limiting: the app's own limiter is in-memory and # resets on restart (server/src/rate-limit.ts). location /api/ { limit_req zone=leads_{{APP}} burst=5 nodelay; limit_req_status 429; proxy_pass http://127.0.0.1:{{PORT}}; include /etc/nginx/snippets/proxy-params.conf; } location / { try_files $uri $uri/ /index.html; } }