diff --git a/README.md b/README.md
index 16855209..74acdd16 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ HAMi-WebUI is built upon the [HAMi](https://github.com/Project-HAMi/HAMi) open-s
## Get started
- [Installation guide](docs/installation/helm/index.md)
+- [Serving under a URL sub-path (base path)](docs/installation/sub-path.md)
## Contributing
diff --git a/charts/hami-webui/README.md b/charts/hami-webui/README.md
index 6c90ec02..c054819a 100644
--- a/charts/hami-webui/README.md
+++ b/charts/hami-webui/README.md
@@ -47,6 +47,7 @@ The command removes all the Kubernetes components associated with the chart and
| Key | Type | Default | Description |
|-----|------|------------------------------------------------------------------------------------|-------------|
| affinity | object | `{}` | |
+| basePath | string | `"/"` | URL sub-path to serve the WebUI under, injected as `HAMI_WEBUI_BASE_PATH`. `"/"` = root (default). Set e.g. `"/gpu-ui/"` for a non-stripping reverse proxy. Resolved at request time (no image rebuild). A path-stripping proxy that sets `X-Forwarded-Prefix` works without this. |
| dcgm-exporter.enabled | bool | `true` | |
| dcgm-exporter.nodeSelector.gpu | string | `"on"` | |
| dcgm-exporter.serviceMonitor.additionalLabels.jobRelease | string | `"hami-webui-prometheus"` | |
@@ -163,7 +164,46 @@ kube-prometheus-stack:
```
This allows you to reuse the existing Operator and CRDs while deploying a new Prometheus instance.
-### 3. About `jobRelease` Labels
+### 3. Serving under a URL sub-path (base path)
+
+By default the WebUI is served at the site root (`/`). To serve it behind a
+reverse-proxy prefix such as `https://host/gpu-ui/` — without rebuilding the
+frontend image — set the base path. It is resolved at **request time**, so the
+same image works at any path.
+
+There are two supported reverse-proxy modes:
+
+**Mode A — path-stripping proxy (recommended, zero chart config).**
+If your proxy strips the prefix before forwarding and sets the
+`X-Forwarded-Prefix` header (nginx `proxy_set_header X-Forwarded-Prefix /gpu-ui;`,
+Traefik `stripPrefix` + headers middleware, etc.), the WebUI picks the prefix up
+from the header automatically. Leave `basePath` at its default `/`.
+
+**Mode B — proxy passes the full prefixed path through (no stripping).**
+Set the chart value so the BFF knows its own prefix:
+
+```yaml
+basePath: "/gpu-ui/"
+# If you also expose it through this chart's ingress, point the path at the same prefix:
+ingress:
+ enabled: true
+ hosts:
+ - host: your-host
+ paths:
+ - path: /gpu-ui
+ pathType: Prefix
+```
+
+`basePath` is injected into the frontend BFF container as the
+`HAMI_WEBUI_BASE_PATH` environment variable. Values are normalized to a
+leading/trailing-slash form (`gpu-ui` → `/gpu-ui/`); `/` (the default) means
+root serving and preserves the historical behaviour exactly. The header (Mode A)
+takes precedence over the env var when both are present.
+
+No changes are needed on the Go API backend — it is always reached via the BFF's
+`/api/vgpu` proxy.
+
+### 4. About `jobRelease` Labels
If deploying a completely new Prometheus, you can leave the default `jobRelease: hami-webui-prometheus` unchanged.
diff --git a/charts/hami-webui/templates/deployment.yaml b/charts/hami-webui/templates/deployment.yaml
index 83219b90..65796339 100644
--- a/charts/hami-webui/templates/deployment.yaml
+++ b/charts/hami-webui/templates/deployment.yaml
@@ -33,6 +33,8 @@ spec:
imagePullPolicy: {{ .Values.image.frontend.pullPolicy }}
env:
{{- toYaml .Values.env.frontend | nindent 12 }}
+ - name: HAMI_WEBUI_BASE_PATH
+ value: {{ .Values.basePath | default "/" | quote }}
ports:
- name: http
containerPort: 3000
diff --git a/charts/hami-webui/values.yaml b/charts/hami-webui/values.yaml
index e7c93b43..c0f244b9 100644
--- a/charts/hami-webui/values.yaml
+++ b/charts/hami-webui/values.yaml
@@ -49,6 +49,17 @@ securityContext: {}
# runAsNonRoot: true
# runAsUser: 1000
+# URL sub-path (base path) the WebUI is served under. "/" (default) serves at
+# the site root and preserves the historical behaviour exactly. Set to a prefix
+# such as "/gpu-ui/" to serve the WebUI behind a reverse proxy that passes the
+# full prefixed path through (i.e. does NOT strip it). It is injected into the
+# frontend BFF as the HAMI_WEBUI_BASE_PATH env var and resolved at request time,
+# so changing it does NOT require rebuilding the frontend image. A path-stripping
+# proxy that sets the X-Forwarded-Prefix header works without setting this.
+# If you set a non-root basePath, remember to update ingress.hosts[].paths[].path
+# to the same prefix.
+basePath: "/"
+
service:
type: ClusterIP
port: 3000
diff --git a/docs/installation/sub-path.md b/docs/installation/sub-path.md
new file mode 100644
index 00000000..8cbae19b
--- /dev/null
+++ b/docs/installation/sub-path.md
@@ -0,0 +1,141 @@
+# Serving HAMi-WebUI under a URL sub-path
+
+By default HAMi-WebUI is served at the site root (`https://host/`). It can also
+be served under an arbitrary reverse-proxy prefix (a "sub-path" / "base path"),
+e.g. `https://host/gpu-ui/`, so several tools can share one hostname.
+
+The base path is resolved **at request time**, not baked into the frontend
+bundle — so a single `*-fe-oss` image works at any path and changing the path
+never requires an image rebuild. The design mirrors Grafana's
+`serve_from_sub_path` and ArgoCD's `server.rootpath`.
+
+The Go API backend (`*-be-oss`) is unaffected — it is always reached through the
+BFF's `/api/vgpu` proxy and is path-agnostic.
+
+## How the base path is resolved
+
+The frontend BFF (the NestJS `*-fe-oss` container) resolves the prefix from two
+sources, in order of precedence:
+
+1. **`X-Forwarded-Prefix` request header** — set by a path-stripping reverse
+ proxy. Takes precedence when present.
+2. **`HAMI_WEBUI_BASE_PATH` environment variable** — the deploy-time default.
+ Defaults to `/` (root serving, the historical behaviour).
+
+Values are normalized to a leading/trailing-slash form: `gpu-ui`, `/gpu-ui`, and
+`/gpu-ui/` all become `/gpu-ui/`. `/` means root.
+
+On every request for `index.html` the BFF injects the resolved value as:
+
+```html
+
+
+```
+
+The SPA reads `window.__BASE_PATH__` for its router base, its axios `baseURL`,
+and its socket.io `path`; the `` tag makes the relative asset URLs
+(`./static/…`, `./favicon.svg`) resolve under the prefix.
+
+## Two supported proxy modes
+
+### Mode A — path-stripping proxy (sets `X-Forwarded-Prefix`)
+
+The proxy strips the prefix before forwarding to the BFF and advertises it via
+the header. Nothing needs to be configured on the chart — leave `basePath` at
+`/`.
+
+nginx example:
+
+```nginx
+location /gpu-ui/ {
+ proxy_pass http://hami-webui:3000/; # trailing slash strips /gpu-ui/
+ proxy_set_header X-Forwarded-Prefix /gpu-ui;
+ proxy_set_header Host $host;
+ # socket.io upgrade (optional, for real-time views)
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+}
+```
+
+### Mode B — proxy forwards the full prefixed path (no stripping)
+
+The BFF receives `/gpu-ui/...` and must know its own prefix, so set the chart
+value (or the env var directly):
+
+```yaml
+# values.yaml
+basePath: "/gpu-ui/"
+ingress:
+ enabled: true
+ hosts:
+ - host: your-host
+ paths:
+ - path: /gpu-ui
+ pathType: Prefix
+```
+
+The BFF strips the configured prefix from incoming URLs up-front, so static
+assets, the `/api/vgpu` proxy and the SPA deep-link fallback all keep working.
+
+## Helm value
+
+| Value | Default | Description |
+|------------|---------|-----------------------------------------------------------------------------|
+| `basePath` | `"/"` | Sub-path to serve under. Injected as `HAMI_WEBUI_BASE_PATH`. `"/"` = root. |
+
+---
+
+## Manual test plan
+
+Prerequisites: a running cluster (or local `node dist/main`) with the built
+frontend in `public/`. Replace `HOST` accordingly.
+
+### 1. Root serving — no regression (default)
+
+Deploy with defaults (`basePath: "/"`, no header).
+
+| Check | Expect |
+|-------|--------|
+| `GET /` | 200; HTML contains `` and `window.__BASE_PATH__="/"` |
+| Deep-link refresh `GET /admin/vgpu/monitor/overview` | 200; same injected `` |
+| `GET /static/.js` | 200 |
+| `GET /favicon.svg` | 200 |
+| `GET /api/vgpu/v1/summary` (POST from UI) | proxied to the Go backend |
+| `GET /health_check` | `{"code":0,...,"data":"OK"}` |
+| Browser: open `/`, navigate the app, open a real-time view | assets load, REST works, socket.io connects on `/socket.io` |
+
+### 2. Mode B — non-stripping proxy under `/gpu-ui/`
+
+Deploy with `basePath: "/gpu-ui/"` and an ingress path of `/gpu-ui`.
+
+| Check | Expect |
+|-------|--------|
+| `GET /gpu-ui/` and `GET /gpu-ui` | 200; ``, `window.__BASE_PATH__="/gpu-ui/"` |
+| Deep-link refresh `GET /gpu-ui/admin/vgpu/monitor/overview` | 200; injected base `/gpu-ui/` |
+| `GET /gpu-ui/static/.js` | 200 |
+| `GET /gpu-ui/favicon.svg` | 200 |
+| UI REST calls | requested at `/gpu-ui/api/vgpu/...`, proxied to the backend |
+| Real-time view | socket.io connects at `/gpu-ui/socket.io` |
+| `GET /health_check` (unprefixed k8s probe) | still `...,"data":"OK"` |
+
+### 3. Mode A — path-stripping proxy via `X-Forwarded-Prefix`
+
+Deploy with defaults (`basePath: "/"`) behind a proxy that strips `/gpu-ui` and
+sets `X-Forwarded-Prefix: /gpu-ui`.
+
+| Check | Expect |
+|-------|--------|
+| `curl -H 'X-Forwarded-Prefix: /gpu-ui' https://HOST/` | ``, `window.__BASE_PATH__="/gpu-ui/"` |
+| Deep-link (proxy has already stripped path) `curl -H 'X-Forwarded-Prefix: /gpu-ui' https://HOST/admin/...` | injected base `/gpu-ui/` |
+| Through the browser at `https://HOST/gpu-ui/` | page loads, assets/REST/socket.io all resolve under `/gpu-ui/` |
+
+### 4. Security
+
+| Check | Expect |
+|-------|--------|
+| `curl -H 'X-Forwarded-Prefix: /x">' https://HOST/` | injected `` is sanitized (no `"`, `<`, `>`); no script injection |
+
+A scripted version of checks 1–3 (and 4) is exercised by the BFF unit tests in
+`src/utils/base-path.spec.ts` and by running `node dist/main` with
+`HAMI_WEBUI_BASE_PATH` / `X-Forwarded-Prefix` against a `public/` build.
diff --git a/packages/web/index.html b/packages/web/index.html
index 4bc0a372..42d54936 100644
--- a/packages/web/index.html
+++ b/packages/web/index.html
@@ -4,8 +4,13 @@
-
-
+
+
HAMi
@@ -14,6 +19,6 @@
-
+