Skip to content

Commit f50eca0

Browse files
committed
fix(helm): let the UI serve the server's index (ui.useServerIndex)
The web UI deployed as an independent instance with its own PVC (…-ui-data), while the init/reindex Jobs only ever index the server's PVC (…-server-data) over HTTP. Nothing populated the UI's volume, so a deployed UI — especially in demo mode, where the in-app Reindex button is disabled — showed an empty index (0 files / 0 chunks / 0 vectors) even though the server was fully indexed. Add ui.useServerIndex: when true the UI mounts the server's index volume read-only at CODERAG_STORE_DIR, so it serves exactly what the index Jobs built and can never corrupt the single-writer LanceDB store. No …-ui-data PVC is created. The embedding-model cache is redirected to the writable home volume so query embedding still works under the read-only mount. ui.coLocateWithServer pins the UI onto the server's node via podAffinity, required when the shared volume is ReadWriteOnce; omit it for a ReadWriteMany storageClass. Rendering fails fast if useServerIndex is set without server.enabled or persistence.enabled. Docs and CI full-values updated to exercise the shared topology. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPY56pX54CybUWA9ibdHny
1 parent 3466ead commit f50eca0

6 files changed

Lines changed: 126 additions & 15 deletions

File tree

deploy/README.md

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,17 @@ processes writing one index would corrupt it. The chart is built around that fac
4646
- **Hardened by default**: non-root (uid 10001), read-only root filesystem, dropped
4747
capabilities, `RuntimeDefault` seccomp, and the service-account token is not mounted.
4848

49-
The **server** is the primary, recommended surface. The **UI** is optional and, because it
50-
bundles the engine and writes its own index, runs as an *independent* instance with its
51-
own volume when enabled (build its index with the in-app **Reindex** button).
49+
The **server** is the primary, recommended surface. The **UI** is optional and runs in one
50+
of two topologies:
51+
52+
- **Shared (recommended for a read-only/demo UI):** `ui.useServerIndex=true` mounts the
53+
server's index volume **read-only**, so the UI serves exactly what the index/reindex
54+
Jobs built — no separate volume, always in sync, and it can never corrupt the writer's
55+
store. Reindexing stays a server-side Job.
56+
- **Independent (default):** the UI gets its **own** data volume and bundles the engine.
57+
Nothing populates that volume automatically (the index Jobs drive the *server's* volume),
58+
so you build it with the in-app **Reindex** button — which is **disabled in demo mode**,
59+
so a demo UI left on the default shows an empty index (0 files / 0 chunks).
5260

5361
---
5462

@@ -142,7 +150,9 @@ Full list with comments: [`values.yaml`](helm/coderag/values.yaml). The most-use
142150
| `server.service.type` | `ClusterIP` | `ClusterIP` · `NodePort` · `LoadBalancer`. |
143151
| `index.initJob.enabled` | `true` | Build the index automatically on install/upgrade. |
144152
| `index.cronjob.enabled` | `false` | Recurring reindex (`index.cronjob.schedule`). |
145-
| `ui.enabled` | `false` | Also deploy the web UI (independent instance). |
153+
| `ui.enabled` | `false` | Also deploy the web UI. |
154+
| `ui.useServerIndex` | `false` | UI serves the **server's** index (read-only) instead of its own empty volume. |
155+
| `ui.coLocateWithServer` | `false` | Pin the UI onto the server's node — required with `useServerIndex` on RWO storage. |
146156
| `ingress.enabled` | `false` | Expose via an Ingress (**add TLS + auth** — the API has none). |
147157
| `resources` (`server.*`, `ui.*`) | see values | CPU/memory requests & limits. |
148158

@@ -406,13 +416,27 @@ ingress:
406416

407417
### Also run the web UI
408418

419+
**Recommended — UI serves the server's index (read-only):**
420+
409421
```bash
410-
--set ui.enabled=true
422+
--set ui.enabled=true \
423+
--set ui.useServerIndex=true
424+
# On ReadWriteOnce storage (the default), also pin the UI onto the server's node:
425+
--set ui.coLocateWithServer=true
426+
# Omit coLocateWithServer if persistence uses a ReadWriteMany storageClass.
411427
```
412428

413-
The UI gets its own data volume and clones the same repo. Open it via port-forward
414-
(`svc/coderag-ui:8501`) or add an Ingress path with `service: ui`, then click **Reindex**
415-
in the sidebar to build its index.
429+
The UI mounts the server's index volume read-only, so it shows whatever the
430+
init/reindex Jobs built — nothing to reindex from the UI, and it stays in sync with the
431+
server. This is the right choice for a public/demo UI, where the in-app Reindex button is
432+
disabled. Open it via port-forward (`svc/coderag-ui:8501`) or an Ingress path with
433+
`service: ui`.
434+
435+
> **Independent UI (default, `ui.useServerIndex=false`):** the UI gets its **own** data
436+
> volume and clones the same repo, but **nothing populates that volume** — you must click
437+
> **Reindex** in the sidebar to build it (impossible in demo mode). If your UI shows
438+
> 0 files / 0 chunks, this is almost always why: the index Jobs filled the *server's*
439+
> volume, not the UI's. Switch to `ui.useServerIndex=true`.
416440

417441
### Pin to an immutable image (reproducible / air-gapped)
418442

@@ -489,12 +513,19 @@ helm template coderag deploy/helm/coderag -f deploy/helm/coderag/ci/full-values.
489513
`/data`, and `/home/coderag`. If a backend insists on another path, mount it via
490514
`extraVolumes`/`extraVolumeMounts`, or relax the hardening:
491515
`--set securityContext.readOnlyRootFilesystem=false`.
516+
- **UI shows 0 files / 0 chunks / 0 vectors** — the UI is on its own (empty) data volume
517+
while the index Jobs populated the *server's* volume. They are different PVCs
518+
(`…-ui-data` vs `…-server-data`). Set `ui.useServerIndex=true` so the UI serves the
519+
server's index read-only (add `ui.coLocateWithServer=true` on `ReadWriteOnce` storage).
520+
The independent UI only fills its own volume via the in-app **Reindex** button, which is
521+
disabled in demo mode.
492522

493523
## Limitations
494524

495525
- **Single writer by design** — do not raise `replicas`. For higher search throughput,
496526
put a cache/load balancer in front of the read endpoints; the index itself stays
497527
single-writer.
498528
- **`ReadWriteOnce`** ties the index to one node at a time; that's expected for the embedded store.
499-
- The **UI**, when enabled, maintains a *separate* index from the server. For a single
500-
shared index, run the server and point browsers/tools at its REST API.
529+
- The **UI**, when enabled, defaults to a *separate* index from the server. For a single
530+
shared index, set `ui.useServerIndex=true` (the UI reads the server's volume read-only),
531+
or run the server alone and point browsers/tools at its REST API.

deploy/helm/coderag/ci/full-values.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ workspace:
1212

1313
ui:
1414
enabled: true
15+
# Exercise the shared-index topology: UI mounts the server's volume read-only (no
16+
# …-ui-data PVC), with node co-location for the ReadWriteOnce volume.
17+
useServerIndex: true
18+
coLocateWithServer: true
1519

1620
secrets:
1721
create: true

deploy/helm/coderag/templates/_helpers.tpl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,19 @@ Service name / in-cluster base URL for the HTTP API (used by the index jobs).
9898
{{- printf "http://%s:%v" (include "coderag.serverServiceName" .) .Values.server.service.port -}}
9999
{{- end -}}
100100

101+
{{/*
102+
Name of the PVC holding the SERVER's index — the chart-managed claim, or the
103+
existingClaim the operator supplied. Used by the server pod and by the UI when
104+
ui.useServerIndex shares that same volume.
105+
*/}}
106+
{{- define "coderag.serverDataClaimName" -}}
107+
{{- if .Values.persistence.existingClaim -}}
108+
{{- .Values.persistence.existingClaim -}}
109+
{{- else -}}
110+
{{- printf "%s-server-data" (include "coderag.fullname" .) -}}
111+
{{- end -}}
112+
{{- end -}}
113+
101114
{{/*
102115
envFrom block shared by the server and UI containers: non-secret config plus the
103116
optional API-key Secret.
@@ -242,6 +255,8 @@ Pod volumes for a writer (server/ui). Call with (dict "ctx" . "component" "serve
242255
persistentVolumeClaim:
243256
{{- if and (eq $component "server") $ctx.Values.persistence.existingClaim }}
244257
claimName: {{ $ctx.Values.persistence.existingClaim }}
258+
{{- else if and (eq $component "ui") $ctx.Values.ui.useServerIndex }}
259+
claimName: {{ include "coderag.serverDataClaimName" $ctx }}
245260
{{- else if and (eq $component "ui") $ctx.Values.ui.persistence.existingClaim }}
246261
claimName: {{ $ctx.Values.ui.persistence.existingClaim }}
247262
{{- else }}

deploy/helm/coderag/templates/ui-deployment.yaml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
{{- if .Values.ui.enabled -}}
2+
{{- if .Values.ui.useServerIndex }}
3+
{{- if not .Values.server.enabled }}
4+
{{- fail "ui.useServerIndex=true requires server.enabled=true (the UI shares the server's index volume)." }}
5+
{{- end }}
6+
{{- if not .Values.persistence.enabled }}
7+
{{- fail "ui.useServerIndex=true requires persistence.enabled=true (there is a server PVC to share; an emptyDir cannot be shared across pods)." }}
8+
{{- end }}
9+
{{- end }}
210
apiVersion: apps/v1
311
kind: Deployment
412
metadata:
@@ -60,6 +68,13 @@ spec:
6068
value: {{ .Values.ui.containerPort | quote }}
6169
- name: HOME
6270
value: /home/coderag
71+
{{- if .Values.ui.useServerIndex }}
72+
# The shared server index is mounted read-only, so the embedding model can't
73+
# be cached onto it. Point the cache at the writable `home` volume instead
74+
# (overrides CODERAG_CACHE_DIR from the ConfigMap) so query embedding works.
75+
- name: CODERAG_CACHE_DIR
76+
value: /home/coderag/.model-cache
77+
{{- end }}
6378
{{- with .Values.ui.extraEnv }}
6479
{{- toYaml . | nindent 12 }}
6580
{{- end }}
@@ -90,6 +105,11 @@ spec:
90105
volumeMounts:
91106
- name: data
92107
mountPath: {{ .Values.persistence.mountPath }}
108+
{{- if .Values.ui.useServerIndex }}
109+
# Shared server index: read-only so the UI can never corrupt the single
110+
# writer's LanceDB store (reindex stays a server-side Job).
111+
readOnly: true
112+
{{- end }}
93113
- name: workspace
94114
mountPath: {{ .Values.workspace.mountPath }}
95115
readOnly: {{ .Values.workspace.readOnly }}
@@ -109,10 +129,27 @@ spec:
109129
nodeSelector:
110130
{{- toYaml . | nindent 8 }}
111131
{{- end }}
132+
{{- if and .Values.ui.useServerIndex .Values.ui.coLocateWithServer }}
133+
# ReadWriteOnce volume shared with the server attaches to a single node, so pin the
134+
# UI pod onto the node already running the server pod. (Set coLocateWithServer=false
135+
# if persistence uses a ReadWriteMany class — then any node can mount the volume.)
136+
affinity:
137+
{{- with .Values.affinity }}
138+
{{- toYaml . | nindent 8 }}
139+
{{- end }}
140+
podAffinity:
141+
requiredDuringSchedulingIgnoredDuringExecution:
142+
- labelSelector:
143+
matchLabels:
144+
{{- include "coderag.selectorLabels" . | nindent 18 }}
145+
app.kubernetes.io/component: server
146+
topologyKey: kubernetes.io/hostname
147+
{{- else }}
112148
{{- with .Values.affinity }}
113149
affinity:
114150
{{- toYaml . | nindent 8 }}
115151
{{- end }}
152+
{{- end }}
116153
{{- with .Values.tolerations }}
117154
tolerations:
118155
{{- toYaml . | nindent 8 }}

deploy/helm/coderag/templates/ui-pvc.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{{- if and .Values.ui.enabled .Values.persistence.enabled (not .Values.ui.persistence.existingClaim) -}}
1+
{{- if and .Values.ui.enabled .Values.persistence.enabled (not .Values.ui.useServerIndex) (not .Values.ui.persistence.existingClaim) -}}
22
apiVersion: v1
33
kind: PersistentVolumeClaim
44
metadata:

deploy/helm/coderag/values.yaml

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,37 @@ server:
171171
# -- Extra env vars (list of {name,value|valueFrom}) for the server container.
172172
extraEnv: []
173173

174-
# --- Web UI (optional; runs as an INDEPENDENT instance with its own index) ---
175-
# The UI bundles the engine and writes its own index, so when enabled it gets a
176-
# separate data volume and workspace. Build its index with the in-app "Reindex"
177-
# button. For a shared, server-maintained index, prefer the server + an ingress.
174+
# --- Web UI (optional) ---
175+
# The UI image bundles the engine and reads a LanceDB store at CODERAG_STORE_DIR.
176+
# Two topologies:
177+
#
178+
# useServerIndex: false (default) — INDEPENDENT instance with its OWN data volume
179+
# (…-ui-data). Nothing populates it automatically: the index/reindex Jobs drive the
180+
# SERVER's volume, not this one. Build it with the in-app "Reindex" button — which is
181+
# DISABLED in demo mode, so a demo UI left on the default will show 0 files/0 chunks.
182+
#
183+
# useServerIndex: true (RECOMMENDED for a read-only / demo UI) — the UI mounts the
184+
# SERVER's index volume READ-ONLY and serves whatever the index Job built. No second
185+
# writer, no separate PVC, always in sync with the server. See `useServerIndex` below.
178186
ui:
179187
enabled: false
180188
containerPort: 8501
189+
# -- Share the SERVER's index instead of keeping a separate (empty) UI volume. When
190+
# true the UI mounts the server's data PVC READ-ONLY at persistence.mountPath, so it
191+
# shows the index built by the init/reindex Jobs and can never corrupt the writer's
192+
# store. No …-ui-data PVC is created. Requirements & caveats:
193+
# * The server (persistence) must be enabled — that's the volume being shared.
194+
# * Access mode: the server PVC is ReadWriteOnce by default, so the UI and server
195+
# pods must land on the SAME node. Either set `coLocateWithServer: true` below, or
196+
# give persistence a ReadWriteMany storageClass (NFS/CephFS/EFS/Longhorn-RWX/…).
197+
# * The UI's model cache is redirected to a writable in-pod volume automatically
198+
# (the shared index mount is read-only), so query embedding still works.
199+
# * Reindex stays a SERVER action (Job/CronJob); the in-app button is irrelevant here.
200+
useServerIndex: false
201+
# -- Pin the UI pod onto the same node as the server pod via podAffinity. REQUIRED with
202+
# useServerIndex on ReadWriteOnce storage (a single RWO volume attaches to one node).
203+
# Harmless (but unnecessary) on ReadWriteMany. Ignored when useServerIndex is false.
204+
coLocateWithServer: false
181205
# -- Deployment update strategy. Defaults to Recreate: the UI is a single writer on
182206
# a ReadWriteOnce volume, so the old pod must release the claim before the new one
183207
# binds it. The cost is a brief gap with no Ready pod on every image change — visible

0 commit comments

Comments
 (0)