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 host.docker.internal:8080 resolve; keepalive 16; } server { listen ${NGINX_PORT} default_server; server_name localhost; 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 to local backend running on host machine 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; } # MCP OAuth surfaces — mirrors nginx_default_local.conf (see the rationale # there). Exact-match consent stays on the FE; everything else proxies to # the backend. Keep these three blocks in sync across the local configs. location = /oauth/consent { try_files /dev/null /index.html; } 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; } 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; }