You want https://example.com/grafana/ to reach an app running on 127.0.0.1:3000. The whole behaviour hinges on one trailing slash on proxy_pass.
Strip the /grafana/ prefix (the app sees /):
location /grafana/ {
proxy_pass http://127.0.0.1:3000/; # <-- trailing slash strips the location prefix
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 $scheme;
}
Keep the /grafana/ prefix (the app sees /grafana/…):
location /grafana/ {
proxy_pass http://127.0.0.1:3000; # <-- no trailing slash, full URI passed through
# ...same proxy_set_header lines...
}
The rule:
proxy_passwith a URI part (even just/) → Nginx replaces the matchedlocationprefix with that URI./grafana/dashboards→/dashboards.proxy_passwith no URI part → the original request URI is passed unchanged./grafana/dashboards→/grafana/dashboards.
The real gotcha is the app’s own links. If it generates absolute URLs like /static/app.js, stripping the prefix breaks them. Most apps have a “base path” / “root URL” setting — set it to /grafana/ and use the keep-the-prefix form. Tell the app its public base with the forwarded headers above (many read X-Forwarded-Prefix):
proxy_set_header X-Forwarded-Prefix /grafana;
sudo nginx -t && sudo nginx -s reload