server { listen 8080; listen [::]:8080; server_name _; root /usr/share/nginx/html; index index.html; # We sit behind Traefik on a different scheme/port. nginx defaults to # `absolute_redirect on`, which would rewrite a `return 302 /es/;` to an # absolute `http://:8080/es/` — and the browser would then # follow that to the internal port, bypassing the proxy. # Sending only the path lets the browser resolve it against the public # URL it actually used (https://familia.example.com/...). absolute_redirect off; # Bigger proxy buffers for the API responses. The /signin-oidc 302 returns # a session cookie chunked across `familynido.session*` cookies, easily # 6–10 KB of headers. With the default 4–8 KB buffer nginx would abort # with `upstream sent too big header` and surface a 502. proxy_buffer_size 16k; proxy_buffers 8 16k; proxy_busy_buffers_size 32k; # Bigger *request* header buffers. The OIDC callback receives a long # `?state=...` plus the browser's Cookie header (session + correlation # cookies + sometimes orphan nonces from prior failed attempts). The # default 4×8K trips the moment any of those grow, returning a stark # 400 "Request Header Or Cookie Too Large" before the request even # reaches the backend. large_client_header_buffers 8 32k; # Allow image uploads up to ~12 MB. The API enforces its own 10 MB cap # (FilesOptions.MaxImageBytes); we pad slightly so a borderline file gets # nginx out of the way and surfaces the API's clean validation error # instead of a blunt 413 with no body. iOS HEIC photos routinely sit # in the 3–8 MB range and would otherwise fail silently. client_max_body_size 12m; # Gzip text assets; PWA manifest explicitly included. gzip on; gzip_comp_level 6; gzip_min_length 1024; gzip_types text/plain text/css application/javascript application/json application/manifest+json image/svg+xml text/xml; # Aggressive caching for hashed bundle files (Angular outputHashing=all). location ~* \.(?:js|css|woff2?|ttf|otf|eot|ico|svg|png|jpg|jpeg|gif|webp|avif)$ { expires 1y; add_header Cache-Control "public, immutable"; try_files $uri =404; } # Service worker registration script and config must never be cached. location ~* (ngsw-worker\.js|ngsw\.json|safety-worker\.js|worker-basic\.min\.js|manifest\.webmanifest)$ { add_header Cache-Control "no-cache, no-store, must-revalidate"; try_files $uri =404; } # Pass /api through to the backend container (name "api" in compose). location /api/ { proxy_pass http://api:8080; 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; # Preserve the scheme Traefik announced to the outside world # (https) instead of overwriting it with nginx's own $scheme (http). proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; proxy_read_timeout 60s; } # OIDC callback paths handled by the backend. Need the same forwarded # headers as /api/ so the OIDC middleware validates the callback against # the public host/scheme and the AuthenticationProperties round-trip works. location = /signin-oidc { proxy_pass http://api:8080; 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 $http_x_forwarded_proto; } location = /signout-callback-oidc { proxy_pass http://api:8080; 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 $http_x_forwarded_proto; } location = /healthz { access_log off; return 200 "ok\n"; add_header Content-Type text/plain; } # ── Locale routing (i18n subpath layout) ──────────────────────────────── # Build emits two bundles: dist/.../browser/es/ and dist/.../browser/en/. # The Dockerfile copies the entire `browser/` tree to /usr/share/nginx/html # so the container ends up with /usr/share/nginx/html/{es,en}/index.html. # # Root visit: pick es or en from Accept-Language and redirect. Anyone # bookmarked at /es/... or /en/... hits the SPA below directly. location = / { # Negotiate locale from Accept-Language. Defaults to es-ES (the # source locale) for any browser that doesn't explicitly prefer # English. Add more `if` branches as locales grow. if ($http_accept_language ~* "^en") { return 302 /en/; } return 302 /es/; } # SPA fallback per locale: any unknown path under /es/ or /en/ falls # through to that bundle's index.html so the Angular router takes over. location /es/ { try_files $uri $uri/ /es/index.html; } location /en/ { try_files $uri $uri/ /en/index.html; } # Catch-all for unprefixed paths. Anything we didn't match above (and # that isn't /api/ or one of the OIDC callbacks) is a stray client # request — typically a stale `returnUrl` like `/home` from before the # /es//en/ split. Redirect it through to the Spanish bundle so the # SPA can take over instead of nginx returning its default 404. location / { if ($http_accept_language ~* "^en") { return 302 /en$request_uri; } return 302 /es$request_uri; } }