Issue Description
When deploying Stalwart behind a reverse proxy at a subpath (e.g. hxxps://example.com/stalwart/), the admin WebUI fails to load its JavaScript and CSS assets because the compiled HTML references them with absolute paths (e.g. src=“/js/chunk-vendors.8210066c.js”) rather than relative ones.
STALWART_PUBLIC_URL correctly updates JMAP/API discovery URLs but has no effect on the admin SPA asset paths.
The issue is even worse without prefix-stripping in nginx, requests to /stalwart/admin/ resulting in 404 directly.
Environment
Stalwart version: v0.16.11
Deployment: Docker Compose, nginx reverse proxy
Expected Behavior
Asset paths in the admin HTML should either be relative, or the server should honour the path component of STALWART_PUBLIC_URL as the base path for the admin SPA build (analogous to Vite’s base / webpack’s publicPath).
Actual Behavior
Browser attempts to load hxxps://example.com/js/chunk-vendors..js instead of hxxps://example.com/stalwart/js/chunk-vendors..js
Reproduction Steps
- Set STALWART_PUBLIC_URL=hxxps://example.com/stalwart
- Configure nginx to proxy location /stalwart/ → hxxp://stalwart:8080/ (with prefix-stripping)
- Navigate to hxxps://example.com/stalwart/admin/
Stalwart Version
v0.16.x
Installation Method
Docker
Database Backend
RocksDB
Blob Storage
RocksDB
Search Engine
Internal
Directory Backend
Internal
I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.
on
I have searched this support forum (open and closed topics) and confirm this is not a duplicate.
on
I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.
on
Let me look into how the WebUI handles asset paths and whether there’s any existing configuration for base paths.
Change the application configuration to use stalwart instead of admin and account so the paths are written properly. If that does not work, you will have to implement rewriting in your proxy.
Hello! To get access to the webapp, I reverted the public URL to default and connected through IP address (without this I could login but got 404 API errors). In the webinterface, going to Settings > WebApplication, I changed the URL prefix (adding /stalwart, deleting /admin and /account). This allowed me to connect with hxxp://IP:8080/stalwart, but not with hxxps://domain/stalwart. Now I cannot get a correct connection: the only URL that allow me to connect is hxxp://IP:8080/stalwart, but I get JSON parse error on every page.
What is the correct configuration (nginx + stalwart) to access the webinterface on a subpath?
The assets are already served relative (the build uses base “./”), and the server does support a subpath: when it serves the SPA under an Application URL prefix it rewrites to , and everything resolves from there. The catch is that the rewrite only fires if the server actually receives the request under that prefix, so nginx must not strip /stalwart/ before proxying.
So set the WebAdmin application’s URL prefix to stalwart (which you did), and use a plain location /stalwart/ { proxy_pass http://backend:8080; } with no path rewrite. If you strip the prefix, the server never sees /stalwart/, the base stays “/”, and you get the absolute paths you saw. The leftover JSON parse error over HTTPS is separate: the API/well-known and OAuth endpoints have to be reachable at the same https://host/stalwart/… base, so make sure the public URL includes the /stalwart path and the proxy forwards it unstripped. I’ll get subpath deployment documented, since it isn’t spelled out today.
Thanks for your answer. I did some more testing but found no correct setup:
- I configured nginx for no rewrite for /stalwart
- As you said /stalwart needs to be added to URL prefix list of the webapp (in the settings) for anything to happen at /stalwart except a 404 (even with STALWART_PUBLIC_URL configured)
- With STALWART_PUBLIC_URL configured with hxxps://domain/stalwart, and /stalwart added to WebAdmin URL prefix list, I can access hxxps://domain/stalwart (login form), which loads /stalwart/assets/index-4bQTQ0fA.js correctly. But when submitting username, a request is done to /api/discover/myusername (instead of /stalwart/api/…) - I checked that STALWART_PUBLIC_URL env var is correct inside the container.
- If I check the openid config as suggested in the docs (curl hxxps://domain/stalwart/.well-known/openid-configuration), I got the Portal landing webpage (asking to load JS assets)
- To be more complete, my TLS cert for hxxps://domain is from LetsEncrpyt, so there shall be no problem with this layer
I’m hosting other services on subpaths for the same domain, so I don’t understand why it doesn’t work for stalwart. Let me know whenever you’ll have this documented, I’ll give it another try.
The working recipe is: nginx location /stalwart/ { proxy_pass http://backend:8080; } with no prefix stripping, the WebAdmin application URL prefix set to stalwart, and STALWART_PUBLIC_URL = https://domain/stalwart. The asset base is served relative and gets rewritten only when the server actually receives the request under /stalwart/, which is why stripping the prefix breaks it.
The two things you’re still hitting, the /api/discover/… call landing at the root instead of under /stalwart, and /stalwart/.well-known/openid-configuration returning the portal page, look like a real gap where the SPA derives the asset base from the prefix but the discover/API and well-known/OAuth base URLs still assume a root mount. Can you post your exact nginx location block and confirm the container sees the path with /stalwart/ intact (a trailing-slash mismatch on proxy_pass or a rewritten Host would produce exactly this)? That will tell me whether it’s the proxy or the SPA.
Hello,
My nginx config related to stalwart is the following:
location /stalwart {
include /config/nginx/proxying.conf;
proxy_pass ``http://12.34.56.78:8080``;
}
Content of proxying.conf (I use it for several services):
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
The nginx logs are as follow:
87.65.43.21 - - [12/Jul/2026:15:32:23 +0000] "GET /stalwart/login HTTP/2.0" 200 466 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0"
87.65.43.21 - - [12/Jul/2026:15:32:24 +0000] "GET /stalwart/assets/index-P0LZg8MO.css HTTP/2.0" 200 47491 "``https://domain/stalwart/login``" "Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0"
87.65.43.21 - - [12/Jul/2026:15:32:24 +0000] "GET /stalwart/assets/index-4bQTQ0fA.js HTTP/2.0" 200 1903110 "``https://domain/stalwart/login``" "Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0"
87.65.43.21 - - [12/Jul/2026:15:32:24 +0000] "GET /logo HTTP/2.0" 200 12398 "``https://domain/stalwart/login``" "Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0"
87.65.43.21 - - [12/Jul/2026:15:32:24 +0000] "GET /stalwart/favicon.svg HTTP/2.0" 200 15406 "``https://domain/stalwart/login``" "Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0"
87.65.43.21 - - [12/Jul/2026:15:33:02 +0000] "GET /api/discover/testuser HTTP/2.0" 200 12398 "``https://domain/stalwart/login``" "Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0"
(client IP and referrer domain replaced)
I haven’t found interesting logs for stalwart container (no log files in default/usual locations and nothing relevant in /var/lib/stalwart/LOG). If you need more information, please tell me where to find it (for context stalwart is run as container).