limit_req_zone $binary_remote_addr zone=global_rate_limit:10m rate=50r/s; # Docker's embedded DNS (127.0.0.11) lets nginx re-resolve upstream service # hostnames. Without `resolver` + a variable-based proxy_pass, nginx resolves # `api` / `ui` to an IP once at startup and caches it forever — if the api # container is rebuilt and Docker gives it a new IP, nginx keeps hitting the # old one and every request returns 502 Bad Gateway until nginx is manually # restarted. `valid=10s` bounds the stale window to ~10 seconds. resolver 127.0.0.11 valid=10s ipv6=off; # Preserve a trusted outer proxy's HTTPS scheme without reflecting arbitrary # client-supplied values into invoice and checkout URLs. map $http_x_forwarded_proto $origin_scheme { default $scheme; http http; https https; } server { listen 80; server_name wowcheckout.such.software localhost; limit_req zone=global_rate_limit burst=200 nodelay; if ($request_uri ~* "//+") { return 308 $scheme://$host$uri$is_args$args; } location /api/ { set $api_upstream "api"; # No URI suffix: nginx preserves the complete /api/v1/... request path. proxy_pass http://$api_upstream:8000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $origin_scheme; } location /api { return 301 /api/; } location = /openapi.json { set $api_upstream "api"; proxy_pass http://$api_upstream:8000/openapi.json; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $origin_scheme; } location /qr/ { alias /qr/; add_header Cache-Control "public, max-age=31536000, immutable"; try_files $uri =404; } location / { set $ui_upstream "ui"; # No trailing-slash URI: with a VARIABLE upstream, a trailing "/" makes # nginx send "/" to the UI for every sub-path (assets, /i/), so # the app's index HTML is returned for /_next/static/*.css (200 text/html) # → the whole UI renders unstyled and client routing dies. Omit the URI so # the full request path is preserved, exactly like the /api/ block above. proxy_pass http://$ui_upstream:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $origin_scheme; } }