client_max_body_size 2G; client_header_buffer_size 16k; large_client_header_buffers 4 64k; # Resolver allows nginx to re-resolve DNS periodically (every 30s) # This allows nginx to start even if backend service is not yet available # Docker's internal DNS resolver is typically at 127.0.0.11 resolver 127.0.0.11 valid=30s; resolver_timeout 2s; upstream backend { zone backend 64k; server backend:8080 resolve; keepalive 16; } server { listen ${NGINX_PORT} default_server; server_name _; # wildcard server name root /usr/share/nginx/html; index index.html; # Dedicated healthcheck endpoint # Returns 200 OK if nginx is running and can serve requests location /health { add_header Content-Type text/plain; access_log off; # Explicitly disable tracing for health checks to avoid unnecessary trace data, # even if global tracing is enabled via OTEL_TRACE. Health endpoints are hit frequently # by monitoring systems and should not be traced. otel_trace off; return 200 "healthy\n"; } # The /api/ location uses a named location (@api) with try_files indirection. # This pattern is preferred over a direct 'location /api/' proxy_pass because: # - It allows for more flexible error handling and fallback logic. # - It separates static file serving from API proxying, ensuring that only requests # that do not match a static file are proxied to the backend. # - It makes the routing behavior explicit and maintainable for future changes. # See: https://nginx.org/en/docs/http/ngx_http_core_module.html#try_files location @api { rewrite /api/(.*) /$1 break; proxy_pass http://backend; 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_read_timeout 90; proxy_connect_timeout 90; proxy_send_timeout 90; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } location /api/ { try_files /dev/null @api; } # OAuth surfaces live at the deployment's natural root namespace, sibling # to the data API at /api/* โ€” NOT nested under it. The issuer URL is the # deployment root (e.g. http://localhost:5173 locally; https://host/opik # in prod). This matches industry OAuth conventions (GitHub /login/oauth/*, # Auth0 /oauth/*) and avoids the consent-redirect dead-end you get when # the issuer points at the API namespace (consent_redirect = baseUrl + # "/oauth/consent" would land at /api/oauth/consent which doesn't exist # on the FE). # # Exact match for the consent UI โ€” React Router renders /oauth/consent # from the SPA bundle. Exact-match (= ) locations always win over prefix # matches in nginx regardless of block order, so consent stays on the FE # while all other /oauth/* paths proxy to the backend. location = /oauth/consent { try_files /dev/null /index.html; } # AS endpoints (/oauth/authorize, /oauth/authorize/context, /oauth/token, # /oauth/revoke, /oauth/register). Backend's Jersey resources mount these # paths directly (no /api prefix in @Path), so no rewrite is needed. # Mirrors configmap-frontend-nginx.yaml (^~ + proxy_redirect off); keep in sync. # These endpoints are plain request/response โ€” no websocket upgrade. location ^~ /oauth/ { proxy_pass http://backend; proxy_redirect off; 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_read_timeout 90; proxy_connect_timeout 90; proxy_send_timeout 90; } # RFC 8414 ยง3 discovery URL for issuer = deployment root. Backend mounts # /.well-known/oauth-authorization-server directly, so we just proxy # without rewrite. Without this route the request falls through to the # SPA catch-all and returns text/html, which breaks any spec-compliant # OAuth client. This is a small metadata GET, so it intentionally omits the # upgrade/timeout directives used by the /oauth/ proxy block above. location = /.well-known/oauth-authorization-server { proxy_pass http://backend; proxy_redirect off; 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; } location / { try_files $uri $uri/ /index.html; } # Add trace ID header for OpenTelemetry add_header X-Trace-ID $otel_trace_id; # Consent screens are prime clickjacking targets; deny framing app-wide. Set at the # server level (not per-location) since nginx add_header is all-or-nothing per context. add_header X-Frame-Options "DENY" always; }