Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/helm-chart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ jobs:
- name: Render default values
run: helm template lobu charts/lobu --namespace lobu >/tmp/lobu-default.yaml

- name: Render install example
run: helm template lobu charts/lobu --namespace lobu -f charts/lobu/values.example.yaml >/tmp/lobu-example.yaml

publish:
needs: lint
if: ${{ github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.publish) }}
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,11 @@ Single-process Node remains the simplest deployment: run it with `node`, `pm2`,
```bash
helm install lobu oci://ghcr.io/lobu-ai/charts/lobu \
--namespace lobu --create-namespace \
-f charts/lobu/values.example.yaml
-f your-values.yaml
```
See `charts/lobu/values.yaml` for the full set of tunables. At minimum supply an
ingress host, a `secretName` Secret containing `DATABASE_URL` + `JWT_SECRET` +
`BETTER_AUTH_SECRET` + provider API keys, and a `database.existingSecret`.

## Architecture

Expand Down
9 changes: 4 additions & 5 deletions charts/lobu/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
apiVersion: v2
name: lobu
description: Lobu self-hosted AI agent platform
description: Lobu Platform - Never forget anything. User content analysis with AI.
type: application
version: 7.1.0
appVersion: 7.1.0
keywords:
- lobu
- agents
- ai
- memory
- content
- analytics
home: https://github.com/lobu-ai/lobu
sources:
- https://github.com/lobu-ai/lobu
maintainers:
- name: lobu-ai
email: emre@lobu.ai
email: emrekabakci@gmail.com
38 changes: 18 additions & 20 deletions charts/lobu/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
Lobu has been installed.
Insights Platform has been deployed!

1. Application URL:
{{- if .Values.ingress.enabled }}
Application URL:
{{- range .Values.ingress.hosts }}
https://{{ . }}
{{- end }}
https://{{ .Values.ingress.host }}
Comment on lines 4 to +5
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the same ingress key as the chart templates for NOTES URL.

Line 5 references .Values.ingress.host, but the chart changes use .Values.ingress.hosts; this can render an empty/incorrect install URL.

Suggested fix
 {{- if .Values.ingress.enabled }}
-   https://{{ .Values.ingress.host }}
+   https://{{ first .Values.ingress.hosts }}
 {{- else }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{{- if .Values.ingress.enabled }}
Application URL:
{{- range .Values.ingress.hosts }}
https://{{ . }}
{{- end }}
https://{{ .Values.ingress.host }}
{{- if .Values.ingress.enabled }}
https://{{ first .Values.ingress.hosts }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@charts/lobu/templates/NOTES.txt` around lines 4 - 5, The NOTES.txt uses
.Values.ingress.host but the chart defines .Values.ingress.hosts; update the
template (inside the .Values.ingress.enabled conditional) to reference the hosts
array (e.g., use .Values.ingress.hosts[0] or iterate over .Values.ingress.hosts
and render each host) so the installed URL is populated correctly; ensure you
still guard with .Values.ingress.enabled and handle the case when hosts is
empty.

{{- else }}
Port-forward the app service:
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "lobu.fullname" . }}-app {{ .Values.service.port }}:{{ .Values.service.port }}
open http://localhost:{{ .Values.service.port }}
kubectl port-forward svc/{{ include "lobu.fullname" . }}-app {{ .Values.service.port }}:{{ .Values.service.port }}
{{- end }}

Secrets:
{{- $secretName := include "lobu.secretName" . }}
{{- if $secretName }}
Using runtime secret: {{ $secretName }}
2. Worker Status:
{{- if .Values.worker.enabled }}
Worker is enabled with {{ .Values.worker.replicaCount }} replica(s)
kubectl get pods -l app.kubernetes.io/component=worker
{{- else }}
No runtime secret configured. Create a Secret with JWT_SECRET, BETTER_AUTH_SECRET,
provider API keys, and other Lobu settings, then set secretName.
Worker is disabled
{{- end }}

Database:
3. Database:
{{- if .Values.database.existingSecret }}
DATABASE_URL comes from Secret {{ .Values.database.existingSecret }} key {{ .Values.database.existingSecretKey }}.
Using credentials from secret: {{ .Values.database.existingSecret }}
{{- else }}
Ensure DATABASE_URL is present in the runtime secret.
Ensure DATABASE_URL is set in your secrets
{{- end }}

Useful checks:
kubectl -n {{ .Release.Namespace }} get pods -l app.kubernetes.io/instance={{ .Release.Name }}
kubectl -n {{ .Release.Namespace }} logs deploy/{{ include "lobu.fullname" . }}-app
4. Secrets:
{{- if .Values.secretName }}
Using secret: {{ .Values.secretName }}
{{- else }}
WARNING: No secretName configured. Ensure all required env vars are set.
{{- end }}

{{- if and (gt (int (.Values.app.replicaCount | default 1)) 1) (or (not .Values.service.sessionAffinity) (eq (.Values.service.sessionAffinity | default "None") "None")) }}

Expand Down
45 changes: 24 additions & 21 deletions charts/lobu/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ Create a default fully qualified app name.
{{- end }}

{{/*
Create chart name and version as used by labels.
Create chart name and version as used by the chart label.
*/}}
{{- define "lobu.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Common labels.
Common labels
*/}}
{{- define "lobu.labels" -}}
helm.sh/chart: {{ include "lobu.chart" . }}
Expand All @@ -41,54 +41,57 @@ app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels.
Selector labels
*/}}
{{- define "lobu.selectorLabels" -}}
app.kubernetes.io/name: {{ include "lobu.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
App selector labels
*/}}
{{- define "lobu.appSelectorLabels" -}}
{{ include "lobu.selectorLabels" . }}
app.kubernetes.io/component: api
{{- end }}

{{/*
Worker selector labels
*/}}
{{- define "lobu.workerSelectorLabels" -}}
{{ include "lobu.selectorLabels" . }}
app.kubernetes.io/component: worker
{{- end }}

{{/*
Embeddings selector labels
*/}}
{{- define "lobu.embeddingsSelectorLabels" -}}
{{ include "lobu.selectorLabels" . }}
app.kubernetes.io/component: embeddings
{{- end }}

{{/*
Resolve the image tag.
Create the app image name
*/}}
{{- define "lobu.imageTag" -}}
{{- default .Chart.AppVersion .Values.image.tag }}
{{- end }}

{{- define "lobu.appImage" -}}
{{- printf "%s/%s-app:%s" .Values.image.registry .Values.image.repository (include "lobu.imageTag" .) }}
{{- $tag := .Values.image.tag | default .Chart.AppVersion }}
{{- printf "%s/%s-app:%s" .Values.image.registry .Values.image.repository $tag }}
{{- end }}

{{/*
Create the worker image name
*/}}
{{- define "lobu.workerImage" -}}
{{- printf "%s/%s-worker:%s" .Values.image.registry .Values.image.repository (include "lobu.imageTag" .) }}
{{- end }}

{{- define "lobu.embeddingsImage" -}}
{{- printf "%s/%s-embeddings:%s" .Values.image.registry .Values.image.repository (include "lobu.imageTag" .) }}
{{- $tag := .Values.image.tag | default .Chart.AppVersion }}
{{- printf "%s/%s-worker:%s" .Values.image.registry .Values.image.repository $tag }}
{{- end }}

{{/*
The Secret loaded into pods via envFrom, if configured.
Create the embeddings service image name
*/}}
{{- define "lobu.secretName" -}}
{{- if .Values.secrets.create }}
{{- default (printf "%s-secrets" (include "lobu.fullname" .)) .Values.secrets.name }}
{{- else }}
{{- .Values.secretName }}
{{- end }}
{{- define "lobu.embeddingsImage" -}}
{{- $tag := .Values.image.tag | default .Chart.AppVersion }}
{{- printf "%s/%s-embeddings:%s" .Values.image.registry .Values.image.repository $tag }}
{{- end }}
4 changes: 2 additions & 2 deletions charts/lobu/templates/app-pvc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ metadata:
app.kubernetes.io/component: api
spec:
accessModes:
{{- toYaml .Values.app.workspaces.accessModes | nindent 4 }}
- ReadWriteOnce
{{- if .Values.app.workspaces.storageClass }}
storageClassName: {{ .Values.app.workspaces.storageClass | quote }}
storageClassName: {{ .Values.app.workspaces.storageClass }}
{{- end }}
resources:
requests:
Expand Down
113 changes: 16 additions & 97 deletions charts/lobu/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,112 +7,50 @@ metadata:
app.kubernetes.io/component: api
spec:
replicas: {{ .Values.app.replicaCount }}
{{- /*
Deploy strategy resolution:
1. Explicit `app.strategy` override always wins.
2. Else if `app.allowMultiReplica: true` AND workspaces is RWX
(or disabled) → RollingUpdate (maxSurge: 1, maxUnavailable: 0).
This is the operator opt-in path for true blue/green deploys.
3. Else → Recreate (the safe default).

Phase 5 (workspaces PVC is OFF by default + LOBU_SESSION_STORE
defaults to snapshot mode) means most deploys naturally land on the
rolling-update branch as soon as `allowMultiReplica: true` is set.
The RWX check still applies for self-hosters who re-enable the PVC.

Why `allowMultiReplica` is an explicit flag, not auto-detected:
several in-memory components break with >1 gateway replicas OR
during the brief RollingUpdate overlap:
* `SseManager` (gateway/services/sse-manager.ts) — SSE streams are
pod-local; a job claimed by pod B broadcasts to no-one if the
client is on pod A.
* AskUser question routing
(gateway/connections/interaction-bridge.ts:193-214) — pending
questions live in a per-pod Map, button clicks can land on the
wrong pod and be dropped.
* Telegram polling mode (gateway/connections/chat-instance-manager
.ts:610-613) — every replica long-polls the same bot, causing
conflicts.
Snapshot-mode session state and the per-conversation advisory lock
are necessary but not sufficient. The flag forces operators to
acknowledge "I have only webhook-mode Chat connections AND accept
the SSE / AskUser handoff caveats" before opting in.
*/}}
{{- $rwxConfigured := has "ReadWriteMany" (.Values.app.workspaces.accessModes | default (list)) }}
{{- $rollSafe := and .Values.app.allowMultiReplica (or (not .Values.app.workspaces.enabled) $rwxConfigured) }}
{{- if .Values.app.strategy }}
{{- with .Values.app.strategy }}
strategy:
{{- toYaml .Values.app.strategy | nindent 4 }}
{{- else if $rollSafe }}
# Operator opted in via app.allowMultiReplica + RWX workspaces.
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
{{- toYaml . | nindent 4 }}
{{- else }}
# Safe default. RWO PVC + in-memory SSE/AskUser/Telegram-polling
# state make rolling overlap unsafe — see comment above.
{{- if .Values.app.workspaces.enabled }}
strategy:
type: Recreate
{{- end }}
{{- end }}
selector:
matchLabels:
{{- include "lobu.appSelectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "lobu.appSelectorLabels" . | nindent 8 }}
{{- with .Values.app.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.app.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
# Grace period must exceed preStopDelaySeconds + reasonable shutdown
# time for the gateway (graceful_shutdown in server.ts cleans up
# task scheduler, embedded gateway, DB pool, HTTP server). Default
# k8s grace period is 30s; bump so the preStop sleep doesn't eat
# the whole window.
terminationGracePeriodSeconds: {{ .Values.app.terminationGracePeriodSeconds | default 45 }}
containers:
- name: app
image: {{ include "lobu.appImage" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- with .Values.app.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: 8787
- containerPort: 8787
protocol: TCP
env:
{{- range $key, $value := .Values.app.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- if and .Values.ingress.hosts (not (hasKey .Values.app.env "PUBLIC_WEB_URL")) }}
- name: PUBLIC_WEB_URL
{{- if .Values.ingress.hosts }}
- name: BASE_URL
value: {{ printf "https://%s" (first .Values.ingress.hosts) | quote }}
{{- end }}
{{- $workerSmoke := .Values.releaseGates.smokeTest.workerSmoke | default dict }}
{{- if and $workerSmoke (hasKey $workerSmoke "enabled") $workerSmoke.enabled }}
# Pin SMOKE_TEST_ALLOWED_HOST to the in-cluster app Service DNS
# name so /api/internal/smoke/dispatch refuses any request
# whose Host header is not the cluster-internal service. The
# smoke Job hits this exact hostname via its curl URL; public
# ingress traffic always carries the operator's external host
# in Host, so this is the second layer of ingress-bypass
# defense (the first is the x-forwarded-* refusal in the
# route handler).
# Pin SMOKE_TEST_ALLOWED_HOST to the in-cluster app Service
# DNS name so /api/internal/smoke/dispatch refuses any
# request whose Host header is the operator's public
# ingress hostname. Belt-and-braces with the
# x-forwarded-* refusal in the route handler.
- name: SMOKE_TEST_ALLOWED_HOST
value: {{ printf "%s-app" (include "lobu.fullname" .) | quote }}
{{- end }}
Expand Down Expand Up @@ -147,29 +85,10 @@ spec:
key: {{ .Values.database.existingSecretKey }}
{{- end }}
{{- end }}
{{- $secretName := include "lobu.secretName" . }}
{{- if $secretName }}
{{- if .Values.secretName }}
envFrom:
- secretRef:
name: {{ $secretName }}
{{- end }}
{{- /*
PreStop hook is only useful under RollingUpdate (the new pod is
already serving, so deregistering the old pod via Service
endpoint removal + giving downstream caches time to notice it
shrinks the "old pod kept getting traffic during drain" window).
Under `Recreate`, the new pod doesn't start until the old one
fully terminates — adding a preStop sleep would EXTEND the
no-available-server window by its duration. We only emit the
hook when preStopDelaySeconds is explicitly > 0; ops repos
using RollingUpdate set it, Recreate-mode deploys leave it at
the default 0.
*/ -}}
{{- if gt (int (.Values.app.preStopDelaySeconds | default 0)) 0 }}
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep {{ .Values.app.preStopDelaySeconds }}"]
name: {{ .Values.secretName }}
{{- end }}
# Readiness probes the DB too (/health/ready does SELECT 1).
# Failing readiness pulls the pod out of the Service endpoint set
Expand All @@ -179,15 +98,15 @@ spec:
readinessProbe:
httpGet:
path: /health/ready
port: http
port: 8787
initialDelaySeconds: {{ .Values.healthCheck.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.healthCheck.readinessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.healthCheck.readinessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.healthCheck.readinessProbe.failureThreshold }}
livenessProbe:
httpGet:
path: /health
port: http
port: 8787
initialDelaySeconds: {{ .Values.healthCheck.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.healthCheck.livenessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.healthCheck.livenessProbe.timeoutSeconds }}
Expand Down
Loading
Loading