Reverse Proxies wie NGINX ermöglichen es, mehrere Webanwendungen unter einer gemeinsamen Domain bereitzustellen. Während das Weiterleiten von HTTP-Requests meist problemlos funktioniert, entstehen häufig Schwierigkeiten, sobald Anwendungen unter einem URL-Unterpfad statt direkt unter / betrieben werden.
Besonders deutlich wird das bei Frameworks wie Next.js, Express oder Spring Boot.
1. Wie NGINX Request-Pfade verarbeitet
Angenommen, eine Anwendung läuft auf localhost:3000 und soll öffentlich unter /test/app/ erreichbar sein.
location /test/app/ {
proxy_pass http://localhost:3000/;
}Ein Request an
https://example.com/test/app/dashboardwird intern weitergeleitet an
http://localhost:3000/dashboardEntscheidend ist, ob proxy_pass einen URI-Teil enthält:
- Mit URI (z. B. abschließender Slash):
proxy_pass http://localhost:3000/;ersetzt den passenden Location-Präfix/test/app/durch/. - Ohne URI:
proxy_pass http://localhost:3000;reicht den ursprünglichen Request-Pfad unverändert weiter – das Backend sieht also/test/app/dashboard.
Note
Die Regel gilt für jeden URI-Teil, nicht nur für
/:proxy_pass http://localhost:3000/v2/;macht aus/test/app/dashboardden Pfad/v2/dashboard. Ausnahmen: Inlocation-Blöcken mit regulären Ausdrücken, nachrewriteoder bei Variablen inproxy_passfindet diese Ersetzung nicht statt.
Ein Request an /test/app (ohne Slash) passt nicht auf location /test/app/. NGINX beantwortet ihn in diesem Fall automatisch mit einem 301 auf /test/app/.
NGINX kann somit öffentliche und interne URL-Strukturen voneinander trennen.
2. Warum statische Ressourcen Probleme verursachen
Das eigentliche Problem entsteht häufig erst, nachdem die Anwendung erfolgreich HTML zurückgeliefert hat.
<script src="/static/app.js"></script>
<img src="/logo.png">
<a href="/dashboard">Dashboard</a>Der Browser interpretiert diese root-relativen Pfade (beginnend mit /) relativ zur Domain-Wurzel, nicht zum öffentlichen Anwendungspfad.
Statt /test/app/logo.png fordert der Browser /logo.png an. Dieser Request liegt außerhalb der NGINX-Location /test/app/ und landet bei einer anderen Anwendung oder in einem 404.
Echte relative Pfade wie logo.png oder ./logo.png werden dagegen relativ zur aktuellen Seite aufgelöst. Das funktioniert unter einem Präfix, bricht aber bei verschachtelten Routen (/test/app/users/42 → /test/app/users/logo.png).
Das Problem betrifft insbesondere:
- Statische Ressourcen: JavaScript, CSS und Bilder
- Navigation: Links und clientseitiges Routing
- Redirects: HTTP-Weiterleitungen über den
Location-Header - Cookies:
Set-CookiemitPath=/oder einem internen Pfad - API-Aufrufe: beispielsweise
fetch("/api/users") - WebSockets: Verbindungen mit fest definierten Pfaden
Ein reines REST-Backend funktioniert deshalb häufig problemlos hinter beliebigen Präfixen, solange es keine absoluten Links oder Redirects erzeugt. Eine vollständige Webanwendung stellt zusätzliche Anforderungen.
NGINX kann einen Teil davon nachträglich korrigieren:
proxy_redirect(standardmäßig aktiv) schreibtLocation- undRefresh-Header um, die auf dieproxy_pass-URL zeigen – also z. B.http://localhost:3000/login→/test/app/login. Root-relative Redirects wieLocation: /loginerfasst die Standardregel nicht.proxy_cookie_pathpasst denPathvon Cookies an.sub_filterkann Text im Response-Body ersetzen – ein fragiler Notbehelf, der z. B. mit komprimierten Responses und dynamisch erzeugten URLs in JavaScript nicht funktioniert.
Die zentrale Erkenntnis
NGINX kann eingehende Requests umschreiben, aber nicht zuverlässig alle URLs korrigieren, die eine Anwendung an den Browser zurückgibt.
3. X-Forwarded-Prefix als Lösungsansatz
Ein Reverse Proxy kann der Anwendung ihren ursprünglichen öffentlichen Pfad über einen HTTP-Header mitteilen.
location /test/app/ {
proxy_pass http://localhost:3000/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Prefix /test/app;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}Das Backend erhält intern den Request /dashboard, bekommt aber zusätzlich die Information, dass die Anwendung öffentlich unter /test/app erreichbar ist.
Ohne eigene Konfiguration setzt NGINX den Host-Header auf den Upstream (localhost:3000). Deshalb ist X-Forwarded-Host (oder Host $host) nötig, damit das Backend den öffentlichen Hostnamen kennt.
Allerdings ist X-Forwarded-Prefix kein offizieller Standard, sondern ein De-facto-Standard. Auch der standardisierte Forwarded-Header (RFC 7239) kennt kein Feld für den Pfadpräfix. Das jeweilige Framework muss den Header explizit auswerten.
Insbesondere clientseitiger JavaScript-Code kennt diesen Header nicht – der Browser sieht ihn nie. Der Präfix muss dem Frontend auf anderem Weg mitgeteilt werden, etwa über ein <base href>, eine eingebettete Konfiguration oder einen API-Endpunkt.
Sicherheit
Forwarded-Header dürfen nur von vertrauenswürdigen Proxies kommen. Der äußerste Proxy muss vom Client mitgeschickte
Forwarded- undX-Forwarded-*-Header überschreiben oder entfernen. Sonst kann ein Angreifer Host, Schema oder Präfix der generierten URLs manipulieren (z. B. für Redirects auf fremde Domains oder Cache Poisoning).
4. Unterschiede zwischen Next.js, Express und Spring Boot
Next.js
Next.js bietet die Konfiguration basePath:
const nextConfig = {
basePath: "/test/app",
};
export default nextConfig;Dieser Wert wird beim Build festgelegt und in die clientseitigen Bundles eingebettet. Laut Dokumentation kann er ohne neuen Build nicht geändert werden.
Das bedeutet, dass dasselbe Build-Artefakt nicht ohne Weiteres unter beliebigen URL-Präfixen betrieben werden kann.
Wichtig für die NGINX-Konfiguration: Mit basePath erwartet Next.js den Präfix im Request. Der Proxy darf ihn also nicht entfernen:
location /test/app/ {
proxy_pass http://localhost:3000; # ohne URI: Pfad bleibt /test/app/...
}next/link, der Router und die generierten Asset-URLs (/_next/...) berücksichtigen basePath automatisch. Nicht automatisch angepasst werden unter anderem:
srcvonnext/image– derbasePathmuss laut Dokumentation selbst vorangestellt werden- normale
<a>-,<img>- und<script>-Tags fetch("/api/...")-Aufrufe im eigenen Code
Next.js wertet X-Forwarded-Prefix nicht aus. Eine dynamische, requestbasierte Unterstützung wäre gerade für containerisierte Anwendungen interessant.
Express
Express bietet flexible Routing-Mechanismen und kann Router unter bestimmten Pfaden einhängen:
app.use("/test/app", router);So kann der Proxy den Präfix auch einfach unverändert weiterreichen. Innerhalb des Routers liefert req.baseUrl dann den Mount-Pfad.
Express wertet X-Forwarded-Prefix jedoch nicht aus. Auch die Einstellung app.set("trust proxy", ...) betrifft nur X-Forwarded-For, X-Forwarded-Proto und X-Forwarded-Host (also req.ip, req.protocol, req.hostname), nicht den Pfad.
Entwickler müssen selbst sicherstellen, dass Links, Redirects (res.redirect("/login") wird nicht angepasst) und statische Ressourcen den öffentlichen Pfad berücksichtigen.
Spring Boot
Spring Boot unterstützt Forwarded-Header über die Property server.forward-headers-strategy:
server.forward-headers-strategy=frameworkDadurch registriert Spring Boot den ForwardedHeaderFilter aus Spring MVC. Er wrappt den Request so, dass Host, Schema und – falls aktiviert – der Präfix aus Sicht des Clients erscheinen. Davon profitieren unter anderem ServletUriComponentsBuilder, MvcUriComponentsBuilder, Redirects über redirect: und Template-Links wie Thymeleafs @{/...}.
Zu beachten:
nativevs.framework: Mitnativeübernimmt der eingebettete Webserver (z. B. Tomcat) die Auswertung. Läuft die Anwendung auf einer erkannten Cloud-Plattform wie Kubernetes, istnativesogar der Default. FürX-Forwarded-Prefixmit Spring MVC istframeworkder verlässliche Weg.- Version prüfen: In neueren Spring-Versionen (Spring Framework 7 / Spring Boot 4) wird
X-Forwarded-Prefixstandardmäßig ignoriert und muss separat aktiviert werden (beim Filter übersetUseForwardedPrefix(true)). Ältere Versionen werten den Header beiframeworkdirekt aus. - Präfix ersetzt Context-Path: Der Präfix ersetzt den Context-Path der Anwendung, er wird nicht davorgehängt. Läuft die App intern unter
/app1und öffentlich unter/api/app1, muss der ProxyX-Forwarded-Prefix: /api/app1senden.
Im Gegensatz zu Next.js lassen sich Pfade bei Spring Boot zur Laufzeit festlegen: server.servlet.context-path beim Anwendungsstart, X-Forwarded-Prefix sogar pro Request – ohne neuen Build.
Allerdings garantiert auch Spring Boot nicht, dass selbst geschriebene root-relative oder absolute URLs (etwa fest codierte Pfade in statischem HTML oder JavaScript) automatisch korrekt funktionieren.
5. Pfadbestandteile in der Java Servlet API
Die Java Servlet API unterscheidet mehrere Bestandteile eines Request-Pfades.
Beispiel:
https://example.com/myapp/api/usersBei einem Context-Path /myapp und einem Servlet-Mapping /api/* ergeben sich folgende Werte:
| Methode | Ergebnis | Bedeutung |
|---|---|---|
getContextPath() | /myapp | Basispfad der Webanwendung |
getServletPath() | /api | Pfad zum zuständigen Servlet |
getPathInfo() | /users | Zusätzlicher Pfad innerhalb des Servlet-Mappings |
getRequestURI() | /myapp/api/users | Vollständiger Request-Pfad ohne Query-Parameter |
Es gilt: requestURI = contextPath + servletPath + pathInfo (bis auf URL-Encoding).
In einer typischen Spring-Boot-Anwendung ist der DispatcherServlet auf / gemappt. Dann ist getServletPath() der gesamte Pfad nach dem Context-Path (/api/users) und getPathInfo() ist null.
Entfernt NGINX den öffentlichen Präfix vor der Weiterleitung, ist dieser in den ursprünglichen Servlet-Request-Daten nicht mehr enthalten.
Der ForwardedHeaderFilter kann den Request für nachgelagerte Komponenten so aufbereiten, dass getContextPath() und getRequestURI() den öffentlichen Präfix enthalten.
6. Fazit: Ein Build, beliebige Pfade?
Eine Webanwendung, die unter / funktioniert, funktioniert nicht automatisch unter /test/app/.
Dafür müssen drei Ebenen zusammenspielen:
- Reverse Proxy: Leitet eingehende Requests an das richtige Backend weiter und übermittelt den öffentlichen Kontext (Host, Schema, Präfix).
- Backend: Berücksichtigt den öffentlichen Pfad bei der Generierung von URLs, Redirects und Cookies.
- Frontend: Verwendet korrekte Pfade für Navigation, Ressourcen und API-Aufrufe.
Spring Boot bietet bereits Mechanismen, um öffentliche URL-Präfixe zur Laufzeit zu berücksichtigen. Next.js setzt dagegen auf einen beim Build festgelegten basePath.
Gerade bei Docker- und Kubernetes-Deployments, bei denen dasselbe Container-Image in unterschiedlichen Umgebungen eingesetzt werden soll, kann diese Einschränkung relevant werden.
Die entscheidende Architekturfrage lautet daher:
Wie können wir eine Webanwendung vollständig unabhängig von ihrer öffentlichen URL betreiben, sodass dieselbe laufende Instanz gleichzeitig unter beliebigen Hostnamen und Pfadpräfixen erreichbar ist?