feat(self-host): add Helm chart for Kubernetes deployment (#2377)

* Include k8s deployment instructions

* Use helm for deployment

* docs(self-host): add Helm / Kubernetes deployment to quickstart (en + zh)

* fix(helm): gate backend ExternalName alias behind a value

The unprefixed Service/backend in the chart is load-bearing, but as
written it limits the chart to one release per namespace and fails
helm install whenever a Service/backend already exists in the
namespace (without --take-ownership).

Gate the alias behind frontend.compatibility.backendAlias (default
true, so existing installs are unchanged). Operators running a web
image with a patched REMOTE_API_URL can set it to false to drop the
Service entirely. Document the one-release-per-namespace constraint
and the opt-out in values.yaml and the SELF_HOSTING.md Kubernetes
section.

Addresses review item #1 on PR #2377.

* fix(helm): add backend startupProbe so cold installs survive migrations

The entrypoint runs `./migrate up` before serving traffic. On a cold
cluster (Postgres still coming up) this can take minutes, during which
the livenessProbe (initialDelaySeconds 30 / periodSeconds 30) trips and
restarts the pod 1-2 times.

Add a startupProbe on /healthz (failureThreshold 30, periodSeconds 10,
~5 min budget). Kubernetes disables liveness/readiness until it passes,
so migrations finish without the pod being killed, and the aggressive
livenessProbe is untouched for steady-state. Update the SELF_HOSTING.md
install step, which no longer expects 1-2 restarts.

Addresses review item #2 on PR #2377.

* fix(helm): roll backend pods on config/secret change via checksum annotations

envFrom does not watch the referenced ConfigMap/Secret, and helm
upgrade alone does not change the pod template hash, so editing
values.yaml + `helm upgrade` left the old backend pods running stale
config.

Add checksum/config (hash of the rendered configmap.yaml) and
checksum/secret (hash of the live existingSecret via lookup, since it
is created out-of-band and has no chart template) to the backend pod
template. Config edits now actually re-roll the backend on upgrade,
and Secret rotations do too. lookup is empty under
`helm template`/`--dry-run`; that placeholder is harmless and
documented inline.

Addresses review item #3 on PR #2377.

* docs(self-host): sync quickstart with new startupProbe behavior

SELF_HOSTING.md was updated to reflect that the backend now stays
Running but not Ready while Postgres comes up (startupProbe absorbs
it, so no restart), but the EN/ZH quickstart docs still described the
pre-startupProbe behavior of "may restart 1-2 times". Bring them in
line.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
Juan Carranza
2026-05-24 23:53:01 -05:00
committed by GitHub
parent f59c34eea8
commit 3df26ddd28
11 changed files with 839 additions and 2 deletions

View File

@@ -135,6 +135,181 @@ multica daemon status
3. Go to **Settings → Agents** and create a new agent
4. Create an issue and assign it to your agent — it will pick up the task automatically
---
## Kubernetes Deployment (Alternative)
If you already run a Kubernetes cluster, you can deploy Multica there instead of Docker Compose using the Helm chart at [`deploy/helm/multica/`](deploy/helm/multica/). It targets a typical k3s / k8s setup with an Ingress controller and a default `ReadWriteOnce` StorageClass — authored against k3s + Traefik + `local-path`, and should work on any cluster with minor tweaks.
The chart creates the following resources in the target namespace:
- `multica-postgres``pgvector/pgvector:pg17` backed by a 10Gi PVC
- `multica-backend` — Go API/WS server backed by a 5Gi uploads PVC
- `multica-frontend` — Next.js standalone server
- Two `Ingress` resources: one for the web host, one for the backend host
- `multica-config` ConfigMap (rendered from `values.yaml`)
The `multica-secrets` Secret is **not** managed by the chart — you create it once with `kubectl` so real values never need to land in git.
> **One release per namespace:** the prebuilt `multica-web` image bakes `REMOTE_API_URL=http://backend:8080` at build time, so the chart ships an ExternalName Service literally named `backend`. Because that name is unprefixed, you can run only one Multica release per namespace, and `helm install` will fail if a `Service/backend` already exists there (pass `--take-ownership`, or use a dedicated namespace). If you build a web image with a patched `REMOTE_API_URL`, set `frontend.compatibility.backendAlias: false` to drop the alias.
> **Prerequisites:** `kubectl` and `helm` (v3.13+ for `--take-ownership`, or v4+) configured for the target cluster, an Ingress controller (Traefik / NGINX), and a default StorageClass.
### Step 1 — Point hostnames at the cluster
The chart defaults to `multica.dev.lan` (web) and `api.multica.dev.lan` (backend). Pick one of:
- **`/etc/hosts`** on every machine that needs access (developer laptops + the machine running the daemon):
```text
192.168.1.206 multica.dev.lan api.multica.dev.lan
```
Replace `192.168.1.206` with any node IP where your Ingress controller's Service is reachable.
- **Local DNS** (Pi-hole, Unbound, etc.): add A records for both hostnames pointing at the cluster Ingress IP.
To use different hostnames, override the matching values at install time (see [Step 4](#step-4--install-the-chart)) — `ingress.frontend.host`, `ingress.backend.host`, plus `backend.config.appUrl`, `backend.config.frontendOrigin`, `backend.config.localUploadBaseUrl`, and `backend.config.googleRedirectUri`.
### Step 2 — Create the namespace
```bash
kubectl create namespace multica
```
### Step 3 — Create the `multica-secrets` Secret
The chart references this Secret by name. Create it once with random values:
```bash
kubectl -n multica create secret generic multica-secrets \
--from-literal=JWT_SECRET="$(openssl rand -hex 32)" \
--from-literal=POSTGRES_PASSWORD="$(openssl rand -hex 16)" \
--from-literal=RESEND_API_KEY="" \
--from-literal=GOOGLE_CLIENT_SECRET="" \
--from-literal=CLOUDFRONT_PRIVATE_KEY="" \
--from-literal=MULTICA_DEV_VERIFICATION_CODE=""
```
Leave optional values empty for now — you can fill them in later (see [Step 5 — Log In](#step-5--log-in)).
### Step 4 — Install the chart
```bash
helm install multica deploy/helm/multica -n multica
```
To override defaults, copy `deploy/helm/multica/values.yaml`, edit it, and pass it with `-f`:
```bash
cp deploy/helm/multica/values.yaml my-values.yaml
# edit my-values.yaml — e.g. change ingress hosts, image tags, resource limits
helm install multica deploy/helm/multica -n multica -f my-values.yaml
```
Watch the pods come up:
```bash
kubectl -n multica get pods -w
```
On a cold cluster the backend can sit `Running` but not `Ready` for a few minutes while it waits on PostgreSQL and runs migrations — a startupProbe absorbs this, so the pod should not restart. Once the backend reports `Ready`, migrations have completed and `/healthz` returns OK:
```bash
curl -H "Host: api.multica.dev.lan" http://<ingress-ip>/healthz
# {"status":"ok","checks":{"db":"ok","migrations":"ok"}}
```
Then open http://multica.dev.lan in your browser.
### Step 5 — Log In
The chart defaults to `APP_ENV=production` (set in `values.yaml` under `backend.config.appEnv`), and there is no fixed verification code by default. Pick one of the following to log in — the same three options as the Docker setup:
- **Recommended (production):** patch the Secret with a real Resend key, then restart the backend:
```bash
kubectl -n multica patch secret multica-secrets --type=merge \
-p '{"stringData":{"RESEND_API_KEY":"re_xxx"}}'
kubectl -n multica rollout restart deploy/multica-backend
```
Real verification codes will be sent to the email address you enter. See [Advanced Configuration → Email](SELF_HOSTING_ADVANCED.md#email-required-for-authentication).
- **Without email configured:** the verification code is generated server-side and printed to the backend pod logs (look for `[DEV] Verification code for ...:`). Useful for one-off testing.
```bash
kubectl -n multica logs -f deploy/multica-backend | grep "Verification code"
```
- **Deterministic local/private testing:** set `backend.config.appEnv: development` in your values file and `MULTICA_DEV_VERIFICATION_CODE=888888` in the Secret, then `helm upgrade` and restart. This fixed code is ignored when `APP_ENV=production`.
```bash
helm upgrade multica deploy/helm/multica -n multica \
-f my-values.yaml --set backend.config.appEnv=development
kubectl -n multica patch secret multica-secrets --type=merge \
-p '{"stringData":{"MULTICA_DEV_VERIFICATION_CODE":"888888"}}'
kubectl -n multica rollout restart deploy/multica-backend
```
`ALLOW_SIGNUP` and `GOOGLE_CLIENT_ID` likewise live under `backend.config.*` in `values.yaml`. After `helm upgrade`, the backend pod will roll automatically because the ConfigMap hash changes; the web UI reads both from `/api/config` at runtime, so no web rebuild is needed.
> **Warning:** do **not** set `MULTICA_DEV_VERIFICATION_CODE` on a publicly reachable instance — anyone who knows an email address can then log in with that fixed code.
### Step 6 — Install CLI & Start Daemon
The daemon runs on your local machine, not in the cluster. Install the CLI and an AI agent as in [Step 3](#step-3--install-cli--start-daemon) above, then point the CLI at your Ingress hostnames:
```bash
multica setup self-host \
--server-url http://api.multica.dev.lan \
--app-url http://multica.dev.lan
```
Make sure the machine running the daemon has the same `/etc/hosts` (or DNS) entries from [Step 1](#step-1--point-hostnames-at-the-cluster).
### Updating
To pull the latest images without changing the chart version:
```bash
kubectl -n multica rollout restart deploy/multica-backend deploy/multica-frontend
```
To pin a specific Multica release, set the image tags in your values file:
```yaml
images:
backend:
tag: v0.2.4
frontend:
tag: v0.2.4
```
Then upgrade:
```bash
helm upgrade multica deploy/helm/multica -n multica -f my-values.yaml
```
To roll back if an upgrade goes sideways:
```bash
helm -n multica rollback multica
```
### Tearing down
```bash
# Remove the workloads but keep the PVCs and the Secret
helm -n multica uninstall multica
# Wipe everything, including PostgreSQL data and uploads
kubectl delete namespace multica
```
---
## Stopping Services
If you installed via the install script:

View File

@@ -1,6 +1,6 @@
---
title: Self-host quickstart
description: Run Multica on your own server or machine with Docker. Takes about 10 minutes.
description: Run Multica on your own server or machine with Docker (or Helm on Kubernetes). Takes about 10 minutes.
---
import { Callout } from "fumadocs-ui/components/callout";
@@ -18,6 +18,10 @@ Agent **execution** still relies on the [daemon](/daemon-runtimes) you run local
## 1. Pull the project and start the backend
<Callout type="info">
**Already on Kubernetes?** Skip Docker and use the Helm chart instead — jump to [Kubernetes deployment](#kubernetes-deployment-alternative) below, then come back to [Step 4](#4-first-login--create-a-workspace) for first login.
</Callout>
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
@@ -155,6 +159,53 @@ After bringing the proxy up, set `FRONTEND_ORIGIN=https://multica.example.com` i
Same flow as Cloud — see [Cloud quickstart → Steps 5-6](/cloud-quickstart#5-create-an-agent).
## Kubernetes deployment (alternative)
If you already run a Kubernetes cluster, the repo also ships a Helm chart at `deploy/helm/multica/`. It's the equivalent of `make selfhost` for k8s — same backend image, frontend image, and `pgvector/pgvector:pg17` Postgres, packaged as Deployments / Services / Ingresses with one `ConfigMap` rendered from `values.yaml`. Authored against k3s + Traefik + `local-path` and should work on any cluster with an Ingress controller and a default `ReadWriteOnce` StorageClass.
The chart **does not template secret values**. It references a Secret named `multica-secrets` by name, so real JWT / DB / Resend / Google keys never need to live in git or in `values.yaml`. Create the namespace + Secret once with kubectl:
```bash
kubectl create namespace multica
kubectl -n multica create secret generic multica-secrets \
--from-literal=JWT_SECRET="$(openssl rand -hex 32)" \
--from-literal=POSTGRES_PASSWORD="$(openssl rand -hex 16)" \
--from-literal=RESEND_API_KEY="" \
--from-literal=GOOGLE_CLIENT_SECRET="" \
--from-literal=CLOUDFRONT_PRIVATE_KEY="" \
--from-literal=MULTICA_DEV_VERIFICATION_CODE=""
```
Then install the chart:
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
helm install multica deploy/helm/multica -n multica
```
Defaults assume the hostnames `multica.dev.lan` (web) and `api.multica.dev.lan` (backend). Add them to `/etc/hosts` (or local DNS) pointing at any node IP where your Ingress is reachable. To use different hostnames, copy `deploy/helm/multica/values.yaml`, edit `ingress.frontend.host` / `ingress.backend.host` and the matching `backend.config.appUrl` / `frontendOrigin` / `localUploadBaseUrl` / `googleRedirectUri`, then install with `-f my-values.yaml`.
On a cold cluster the backend can stay `Running` but not `Ready` for a few minutes while it waits on Postgres and runs migrations — a startupProbe absorbs this, so the pod should not restart. Once it's `Ready`:
```bash
curl -H "Host: api.multica.dev.lan" http://<ingress-ip>/healthz
# {"status":"ok","checks":{"db":"ok","migrations":"ok"}}
```
Then open `http://multica.dev.lan` and continue at [Step 4 — First login](#4-first-login--create-a-workspace) above. Point the CLI at your Ingress hostnames:
```bash
multica setup self-host \
--server-url http://api.multica.dev.lan \
--app-url http://multica.dev.lan
```
To pull the latest images without changing the chart, `kubectl -n multica rollout restart deploy/multica-backend deploy/multica-frontend`. To pin a specific Multica release, set `images.backend.tag` / `images.frontend.tag` in your values file and `helm upgrade`. `helm -n multica uninstall multica` removes the workloads but keeps the PVCs and Secret; `kubectl delete namespace multica` wipes everything.
The full reference — three login modes, the `backend` ExternalName workaround for the build-time-baked `REMOTE_API_URL` in the web image, resource limits, and TLS — lives in the repo's [`SELF_HOSTING.md`](https://github.com/multica-ai/multica/blob/main/SELF_HOSTING.md#kubernetes-deployment-alternative).
## Common issues
- **Backend won't start**: check container logs with `docker compose -f docker-compose.selfhost.yml logs backend`; usually it's a bad `DATABASE_URL` or `JWT_SECRET` in `.env`

View File

@@ -1,6 +1,6 @@
---
title: Self-Host 快速上手
description: 在自己的服务器或本机用 Docker 把 Multica 跑起来。约 10 分钟。
description: 在自己的服务器或本机用 Docker 把 Multica 跑起来(也可以在 Kubernetes 上用 Helm。约 10 分钟。
---
import { Callout } from "fumadocs-ui/components/callout";
@@ -18,6 +18,10 @@ import { Callout } from "fumadocs-ui/components/callout";
## 1. 拉取项目 + 一键启动后端
<Callout type="info">
**已经有 Kubernetes 集群?** 不用走 Docker直接用 Helm chart——跳到下面的 [Kubernetes 部署(替代方案)](#kubernetes-部署替代方案),装完再回到 [第 4 步](#4-首次登录--创建工作区) 完成登录。
</Callout>
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
@@ -154,6 +158,53 @@ multica.example.com {
流程和 Cloud 一样——见 [Cloud 快速上手 → 5-6 步](/cloud-quickstart#5-创建智能体)。
## Kubernetes 部署(替代方案)
如果你已经在跑 Kubernetes 集群,仓库里也带了一个 Helm chart路径 `deploy/helm/multica/`。它就是 k8s 版的 `make selfhost`——一样的 backend 镜像、frontend 镜像、`pgvector/pgvector:pg17` Postgres封装成 Deployment / Service / Ingress再加上一个由 `values.yaml` 渲染出来的 `ConfigMap`。这套 chart 是按照 k3s + Traefik + `local-path` 写的,集群里只要有 Ingress controller 和默认的 `ReadWriteOnce` StorageClass 就能跑,其他类型的集群稍微改一改也能用。
这个 chart **不会模板化任何敏感值**。它通过 name 引用一个叫 `multica-secrets` 的 Secret所以真实的 JWT / DB / Resend / Google 密钥永远不用进 git也不用进 `values.yaml`。先用 kubectl 一次性把命名空间和 Secret 建好:
```bash
kubectl create namespace multica
kubectl -n multica create secret generic multica-secrets \
--from-literal=JWT_SECRET="$(openssl rand -hex 32)" \
--from-literal=POSTGRES_PASSWORD="$(openssl rand -hex 16)" \
--from-literal=RESEND_API_KEY="" \
--from-literal=GOOGLE_CLIENT_SECRET="" \
--from-literal=CLOUDFRONT_PRIVATE_KEY="" \
--from-literal=MULTICA_DEV_VERIFICATION_CODE=""
```
再装 chart
```bash
git clone https://github.com/multica-ai/multica.git
cd multica
helm install multica deploy/helm/multica -n multica
```
默认主机名是 `multica.dev.lan`web和 `api.multica.dev.lan`backend。把它们加进 `/etc/hosts`(或者本地 DNS指向任意一个 Ingress 可达的节点 IP 就行。要换主机名,就把 `deploy/helm/multica/values.yaml` 复制一份,改掉 `ingress.frontend.host` / `ingress.backend.host`,再把 `backend.config.appUrl` / `frontendOrigin` / `localUploadBaseUrl` / `googleRedirectUri` 改成相应的地址,然后 `helm install ... -f my-values.yaml`。
冷集群上 backend 可能会 `Running` 但 `Not Ready` 持续几分钟,等 Postgres 起来并跑完 migration——startupProbe 会兜住这一段pod 不会被 liveness 重启。等它 `Ready` 之后:
```bash
curl -H "Host: api.multica.dev.lan" http://<ingress-ip>/healthz
# {"status":"ok","checks":{"db":"ok","migrations":"ok"}}
```
然后浏览器打开 `http://multica.dev.lan`,回到上面的 [第 4 步——首次登录](#4-首次登录--创建工作区) 继续。命令行连到你的 Ingress 主机:
```bash
multica setup self-host \
--server-url http://api.multica.dev.lan \
--app-url http://multica.dev.lan
```
只想拉最新镜像、不动 chart`kubectl -n multica rollout restart deploy/multica-backend deploy/multica-frontend`。要锁到某个 Multica 版本,就在 values 文件里设 `images.backend.tag` / `images.frontend.tag`,再 `helm upgrade`。`helm -n multica uninstall multica` 只删工作负载PVC 和 Secret 都保留;`kubectl delete namespace multica` 才会全清。
完整参考——三种登录方式、为了绕过 web 镜像 build-time 写死的 `REMOTE_API_URL` 而加的 `backend` ExternalName 别名、资源限制、TLS——都在仓库的 [`SELF_HOSTING.md`](https://github.com/multica-ai/multica/blob/main/SELF_HOSTING.md#kubernetes-deployment-alternative)。
## 常见问题
- **后端起不来**:看容器日志 `docker compose -f docker-compose.selfhost.yml logs backend`;常见是 `.env` 里 `DATABASE_URL` 或 `JWT_SECRET` 有问题

View File

@@ -0,0 +1,16 @@
apiVersion: v2
name: multica
description: |
AI-native task management — like Linear, but with AI agents as first-class citizens.
Self-host the backend, frontend, and PostgreSQL (pgvector) on a Kubernetes cluster.
type: application
version: 0.1.0
appVersion: "latest"
home: https://multica.ai
sources:
- https://github.com/multica-ai/multica
keywords:
- multica
- issue-tracker
- ai-agents
- self-host

View File

@@ -0,0 +1,35 @@
{{/*
Common labels for all resources.
*/}}
{{- define "multica.labels" -}}
app.kubernetes.io/name: multica
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end -}}
{{/*
Per-component resource names. Using Release.Name keeps the same name we used
under the kustomize layout when installed as `helm install multica ...`.
*/}}
{{- define "multica.backend.fullname" -}}
{{ .Release.Name }}-backend
{{- end -}}
{{- define "multica.frontend.fullname" -}}
{{ .Release.Name }}-frontend
{{- end -}}
{{- define "multica.postgres.fullname" -}}
{{ .Release.Name }}-postgres
{{- end -}}
{{/*
DATABASE_URL pieced together from the postgres service + Secret values.
The $(VAR) syntax is resolved by the kubelet from the container's env, so
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB must also be loaded into env
on the same container (see envFrom on the backend Deployment).
*/}}
{{- define "multica.databaseUrl" -}}
postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@{{ include "multica.postgres.fullname" . }}:5432/$(POSTGRES_DB)?sslmode=disable
{{- end -}}

View File

@@ -0,0 +1,143 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "multica.backend.fullname" . }}-uploads
labels:
{{- include "multica.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: {{ .Values.backend.uploads.persistence.size | quote }}
{{- with .Values.backend.uploads.persistence.storageClass }}
storageClassName: {{ . | quote }}
{{- end }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "multica.backend.fullname" . }}
labels:
{{- include "multica.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
spec:
# The backend entrypoint runs migrations before starting. Keep replicas at 1
# so two pods don't race on startup migrations.
replicas: {{ .Values.backend.replicas }}
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: multica
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: backend
template:
metadata:
annotations:
# Roll the backend pods whenever its config changes. envFrom does not
# watch the referenced objects, and helm upgrade alone won't change the
# pod template hash, so without these checksums editing values.yaml +
# `helm upgrade` would leave the old pods running stale config.
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
# existingSecret is created out-of-band (not a chart template), so we
# checksum the live object via lookup instead of $.Template.BasePath.
# lookup returns empty during `helm template`/`--dry-run`, so the
# rendered value there is a placeholder; on a real install/upgrade it
# reflects the actual Secret and rolls pods on rotation.
checksum/secret: {{ (lookup "v1" "Secret" .Release.Namespace .Values.existingSecret).data | toYaml | sha256sum }}
labels:
{{- include "multica.labels" . | nindent 8 }}
app.kubernetes.io/component: backend
spec:
containers:
- name: backend
image: "{{ .Values.images.backend.repository }}:{{ .Values.images.backend.tag }}"
imagePullPolicy: {{ .Values.images.backend.pullPolicy }}
ports:
- containerPort: 8080
name: http
envFrom:
- configMapRef:
name: {{ .Release.Name }}-config
- secretRef:
name: {{ .Values.existingSecret }}
env:
- name: DATABASE_URL
value: {{ include "multica.databaseUrl" . | quote }}
volumeMounts:
- name: uploads
mountPath: /app/data/uploads
# The entrypoint runs `./migrate up` before serving traffic. On a
# cold cluster (Postgres still coming up) this can take minutes. The
# startupProbe holds off liveness/readiness until /healthz is OK,
# giving migrations a ~5 min budget (30 * 10s) so the pod is never
# killed mid-migration. Once it passes, the aggressive livenessProbe
# below takes over.
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 30
resources:
{{- toYaml .Values.backend.resources | nindent 12 }}
volumes:
- name: uploads
persistentVolumeClaim:
claimName: {{ include "multica.backend.fullname" . }}-uploads
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "multica.backend.fullname" . }}
labels:
{{- include "multica.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: multica
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: backend
ports:
- port: 8080
targetPort: 8080
name: http
{{- if .Values.frontend.compatibility.backendAlias }}
---
# DNS alias: the multica-web image bakes REMOTE_API_URL=http://backend:8080
# at build time, and the Next.js standalone build does not re-evaluate the
# rewrite destinations from runtime env. This ExternalName makes the bare
# host "backend" resolve to the backend Service inside the cluster, so the
# frontend's /api, /ws, /auth, and /uploads proxies work out of the box.
#
# The name is intentionally unprefixed ("backend", not "{{ .Release.Name }}-backend")
# because the baked-in host has no release prefix. As a result only ONE release
# of this chart can run per namespace, and the name may collide with a
# pre-existing Service/backend (see frontend.compatibility.backendAlias in
# values.yaml). Operators running a web image built with a patched
# REMOTE_API_URL can set that value to false to drop this Service entirely.
apiVersion: v1
kind: Service
metadata:
name: backend
labels:
{{- include "multica.labels" . | nindent 4 }}
app.kubernetes.io/component: backend-alias
spec:
type: ExternalName
externalName: {{ printf "%s.%s.svc.cluster.local" (include "multica.backend.fullname" .) .Release.Namespace }}
{{- end }}

View File

@@ -0,0 +1,29 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
labels:
{{- include "multica.labels" . | nindent 4 }}
data:
# --- Backend ---
PORT: "8080"
APP_ENV: {{ .Values.backend.config.appEnv | quote }}
MULTICA_APP_URL: {{ .Values.backend.config.appUrl | quote }}
FRONTEND_ORIGIN: {{ .Values.backend.config.frontendOrigin | quote }}
CORS_ALLOWED_ORIGINS: {{ .Values.backend.config.corsAllowedOrigins | quote }}
COOKIE_DOMAIN: {{ .Values.backend.config.cookieDomain | quote }}
RESEND_FROM_EMAIL: {{ .Values.backend.config.resendFromEmail | quote }}
ALLOW_SIGNUP: {{ .Values.backend.config.allowSignup | quote }}
ALLOWED_EMAILS: {{ .Values.backend.config.allowedEmails | quote }}
ALLOWED_EMAIL_DOMAINS: {{ .Values.backend.config.allowedEmailDomains | quote }}
GOOGLE_CLIENT_ID: {{ .Values.backend.config.googleClientId | quote }}
GOOGLE_REDIRECT_URI: {{ .Values.backend.config.googleRedirectUri | quote }}
S3_BUCKET: {{ .Values.backend.config.s3Bucket | quote }}
S3_REGION: {{ .Values.backend.config.s3Region | quote }}
CLOUDFRONT_DOMAIN: {{ .Values.backend.config.cloudfrontDomain | quote }}
CLOUDFRONT_KEY_PAIR_ID: {{ .Values.backend.config.cloudfrontKeyPairId | quote }}
LOCAL_UPLOAD_BASE_URL: {{ .Values.backend.config.localUploadBaseUrl | quote }}
# --- PostgreSQL (also consumed by the backend to build DATABASE_URL) ---
POSTGRES_DB: {{ .Values.postgres.database | quote }}
POSTGRES_USER: {{ .Values.postgres.user | quote }}

View File

@@ -0,0 +1,52 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "multica.frontend.fullname" . }}
labels:
{{- include "multica.labels" . | nindent 4 }}
app.kubernetes.io/component: frontend
spec:
replicas: {{ .Values.frontend.replicas }}
selector:
matchLabels:
app.kubernetes.io/name: multica
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: frontend
template:
metadata:
labels:
{{- include "multica.labels" . | nindent 8 }}
app.kubernetes.io/component: frontend
spec:
containers:
- name: frontend
image: "{{ .Values.images.frontend.repository }}:{{ .Values.images.frontend.tag }}"
imagePullPolicy: {{ .Values.images.frontend.pullPolicy }}
ports:
- containerPort: 3000
name: http
env:
- name: HOSTNAME
value: "0.0.0.0"
- name: PORT
value: "3000"
resources:
{{- toYaml .Values.frontend.resources | nindent 12 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "multica.frontend.fullname" . }}
labels:
{{- include "multica.labels" . | nindent 4 }}
app.kubernetes.io/component: frontend
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: multica
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: frontend
ports:
- port: 3000
targetPort: 3000
name: http

View File

@@ -0,0 +1,59 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "multica.frontend.fullname" . }}
labels:
{{- include "multica.labels" . | nindent 4 }}
app.kubernetes.io/component: frontend
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
- host: {{ .Values.ingress.frontend.host | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "multica.frontend.fullname" . }}
port:
number: 3000
{{- with .Values.ingress.tls }}
tls:
{{- toYaml . | nindent 4 }}
{{- end }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "multica.backend.fullname" . }}
labels:
{{- include "multica.labels" . | nindent 4 }}
app.kubernetes.io/component: backend
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
- host: {{ .Values.ingress.backend.host | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "multica.backend.fullname" . }}
port:
number: 8080
{{- with .Values.ingress.tls }}
tls:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,99 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "multica.postgres.fullname" . }}-data
labels:
{{- include "multica.labels" . | nindent 4 }}
app.kubernetes.io/component: postgres
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: {{ .Values.postgres.persistence.size | quote }}
{{- with .Values.postgres.persistence.storageClass }}
storageClassName: {{ . | quote }}
{{- end }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "multica.postgres.fullname" . }}
labels:
{{- include "multica.labels" . | nindent 4 }}
app.kubernetes.io/component: postgres
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: multica
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: postgres
template:
metadata:
labels:
{{- include "multica.labels" . | nindent 8 }}
app.kubernetes.io/component: postgres
spec:
containers:
- name: postgres
image: "{{ .Values.images.postgres.repository }}:{{ .Values.images.postgres.tag }}"
imagePullPolicy: {{ .Values.images.postgres.pullPolicy }}
ports:
- containerPort: 5432
name: postgres
env:
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: {{ .Release.Name }}-config
key: POSTGRES_DB
- name: POSTGRES_USER
valueFrom:
configMapKeyRef:
name: {{ .Release.Name }}-config
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.existingSecret }}
key: POSTGRES_PASSWORD
# local-path PVCs mount with a lost+found directory — point PGDATA
# at a subdir so initdb sees an empty target.
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
readinessProbe:
exec:
command:
- sh
- -c
- pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"
initialDelaySeconds: 5
periodSeconds: 5
resources:
{{- toYaml .Values.postgres.resources | nindent 12 }}
volumes:
- name: data
persistentVolumeClaim:
claimName: {{ include "multica.postgres.fullname" . }}-data
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "multica.postgres.fullname" . }}
labels:
{{- include "multica.labels" . | nindent 4 }}
app.kubernetes.io/component: postgres
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: multica
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: postgres
ports:
- port: 5432
targetPort: 5432

View File

@@ -0,0 +1,127 @@
# -----------------------------------------------------------------------------
# Container images
# -----------------------------------------------------------------------------
images:
backend:
repository: ghcr.io/multica-ai/multica-backend
tag: latest
pullPolicy: IfNotPresent
frontend:
repository: ghcr.io/multica-ai/multica-web
tag: latest
pullPolicy: IfNotPresent
postgres:
repository: pgvector/pgvector
tag: pg17
pullPolicy: IfNotPresent
# -----------------------------------------------------------------------------
# Pre-created Secret with sensitive values. Create it before `helm install`:
#
# kubectl -n <namespace> create secret generic multica-secrets \
# --from-literal=JWT_SECRET="$(openssl rand -hex 32)" \
# --from-literal=POSTGRES_PASSWORD="$(openssl rand -hex 16)" \
# --from-literal=RESEND_API_KEY="" \
# --from-literal=GOOGLE_CLIENT_SECRET="" \
# --from-literal=CLOUDFRONT_PRIVATE_KEY="" \
# --from-literal=MULTICA_DEV_VERIFICATION_CODE=""
#
# The chart references this Secret by name; it does not template it, so real
# values never need to land in git.
# -----------------------------------------------------------------------------
existingSecret: multica-secrets
# -----------------------------------------------------------------------------
# PostgreSQL (pgvector)
# -----------------------------------------------------------------------------
postgres:
database: multica
user: multica
persistence:
size: 10Gi
# Leave empty to use the cluster's default StorageClass.
storageClass: ""
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
# -----------------------------------------------------------------------------
# Backend (Go API + WS server)
# -----------------------------------------------------------------------------
backend:
replicas: 1
uploads:
persistence:
size: 5Gi
storageClass: ""
# All non-secret backend env. Secret values come from `existingSecret`.
config:
appEnv: production
appUrl: http://multica.dev.lan
frontendOrigin: http://multica.dev.lan
corsAllowedOrigins: ""
cookieDomain: ""
resendFromEmail: noreply@multica.ai
allowSignup: true
allowedEmails: ""
allowedEmailDomains: ""
googleClientId: ""
googleRedirectUri: http://multica.dev.lan/auth/callback
s3Bucket: ""
s3Region: us-west-2
cloudfrontDomain: ""
cloudfrontKeyPairId: ""
localUploadBaseUrl: http://api.multica.dev.lan
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
# -----------------------------------------------------------------------------
# Frontend (Next.js standalone)
#
# The multica-web image bakes REMOTE_API_URL=http://backend:8080 at build time;
# the chart ships an ExternalName Service named "backend" so that bare host
# resolves to the in-cluster backend Service.
# -----------------------------------------------------------------------------
frontend:
replicas: 1
# Compatibility shim for the prebuilt multica-web image.
compatibility:
# When true (default) the chart creates an ExternalName Service literally
# named "backend" so the REMOTE_API_URL=http://backend:8080 baked into the
# web image resolves in-cluster. Because that name is unprefixed, only ONE
# release of this chart can run per namespace, and it will collide with any
# pre-existing Service/backend (helm install then fails without
# --take-ownership). Set to false if you run a web image built with a
# patched REMOTE_API_URL and don't need the alias.
backendAlias: true
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
# -----------------------------------------------------------------------------
# Ingress
# -----------------------------------------------------------------------------
ingress:
enabled: true
className: traefik
annotations: {}
frontend:
host: multica.dev.lan
backend:
host: api.multica.dev.lan
# tls:
# - hosts: [multica.dev.lan, api.multica.dev.lan]
# secretName: multica-tls