worker_processes auto; # Anchor everything nginx writes under /tmp so the image boots as an # arbitrary uid (OpenShift restricted-v2 assigns one in gid 0; the stock # nginx image's /var/cache/nginx and /run are root-owned 755) and works # with readOnlyRootFilesystem when /tmp is an emptyDir. pid /tmp/nginx.pid; events { worker_connections 1024; } http { client_body_temp_path /tmp/nginx-client-temp; proxy_temp_path /tmp/nginx-proxy-temp; fastcgi_temp_path /tmp/nginx-fastcgi-temp; uwsgi_temp_path /tmp/nginx-uwsgi-temp; scgi_temp_path /tmp/nginx-scgi-temp; include /etc/nginx/mime.types; default_type application/octet-stream; sendfile on; tcp_nopush on; keepalive_timeout 65; gzip on; gzip_comp_level 4; gzip_min_length 1024; gzip_proxied any; gzip_types application/javascript application/json text/css text/html image/svg+xml font/woff font/woff2; server { listen 3000 default_server; server_name _; root /usr/share/nginx/html; # next.config.mjs sets assetPrefix=/litellm-asset-prefix, which makes # the built HTML reference /litellm-asset-prefix/_next/... — but the # static export only emits files under /_next/. Map the prefix to # the real tree at request time instead of duplicating the directory # at build time. NB: alias rewrites the location prefix, so # /litellm-asset-prefix/_next/foo.js → /usr/share/nginx/html/_next/foo.js. location /litellm-asset-prefix/_next/ { alias /usr/share/nginx/html/_next/; expires 1y; add_header Cache-Control "public, immutable"; } # Content-hashed asset bundles — cache forever. location /_next/ { try_files $uri =404; expires 1y; add_header Cache-Control "public, immutable"; } location /assets/ { try_files $uri =404; expires 1y; add_header Cache-Control "public, immutable"; } location ^~ /ui/assets/ { alias /usr/share/nginx/html/assets/; } location = /favicon.ico { try_files $uri =404; expires 1d; } # Probe target — doesn't depend on disk. location = /healthz { default_type text/plain; return 200 "ok\n"; } # Next.js App Router (output: "export") emits an RSC/flight payload # as .txt next to .html, plus __next.*.txt segment # data. The client router fetches these on soft navigation/prefetch # (?_rsc=) — the query string is irrelevant, files resolve by # $uri. These MUST be served from the export: if they fall through # to the catch-all 404 below, client-side navigation never settles # and the login flow spins in an infinite redirect loop # (/ ⇄ /ui/login). Keep this BEFORE the /ui/ regex — ^/ui/(.+)$ is # also a regex and nginx takes the first matching one, so a stray # /ui/.txt would otherwise be rewritten to HTML and break RSC # for nested routes. A genuinely missing payload must 404 (the # router degrades to a hard navigation); never fall back to HTML. location ~ \.txt$ { try_files $uri =404; } # /ui[/] — the dashboard's JS hardcodes URLs under this prefix # (router.replace("/ui"), buildLoginUrlWithReturn("/ui/login"), ...). # Mirror what FastAPI StaticFiles(mount="/ui") did in the monolithic # proxy_server: serve /ui/ from out/.html, with App # Router-aware fallback (out//index.html) and a final SPA # fallback to out/index.html for client-side routes. location = /ui { try_files /index.html =404; } location = /ui/ { try_files /index.html =404; } location ~ ^/ui/(.+)$ { try_files /$1.html /$1/index.html /index.html =404; } # `/` is handy for direct-debug port-forwards. location = / { try_files /index.html =404; } # Anything else (API calls etc.) returns 404 from the UI's # perspective. A reverse proxy in front of this image routes the # API surface (/v1, /key, /.well-known/litellm-ui-config, ...) to # gateway/backend before requests get here; if something slips # through, fall through to a 404 instead of accidentally serving # HTML and confusing a JSON-expecting caller. location / { return 404; } } }