diff --git a/SELF_HOSTING.md b/SELF_HOSTING.md index 786b0c686..b9b5a7c73 100644 --- a/SELF_HOSTING.md +++ b/SELF_HOSTING.md @@ -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:///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: diff --git a/apps/docs/content/docs/self-host-quickstart.mdx b/apps/docs/content/docs/self-host-quickstart.mdx index b7723a7a8..c3700951e 100644 --- a/apps/docs/content/docs/self-host-quickstart.mdx +++ b/apps/docs/content/docs/self-host-quickstart.mdx @@ -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 + +**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. + + ```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:///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` diff --git a/apps/docs/content/docs/self-host-quickstart.zh.mdx b/apps/docs/content/docs/self-host-quickstart.zh.mdx index b021cc267..6808e42e3 100644 --- a/apps/docs/content/docs/self-host-quickstart.zh.mdx +++ b/apps/docs/content/docs/self-host-quickstart.zh.mdx @@ -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. 拉取项目 + 一键启动后端 + +**已经有 Kubernetes 集群?** 不用走 Docker,直接用 Helm chart——跳到下面的 [Kubernetes 部署(替代方案)](#kubernetes-部署替代方案),装完再回到 [第 4 步](#4-首次登录--创建工作区) 完成登录。 + + ```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:///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` 有问题 diff --git a/deploy/helm/multica/Chart.yaml b/deploy/helm/multica/Chart.yaml new file mode 100644 index 000000000..ba8424d69 --- /dev/null +++ b/deploy/helm/multica/Chart.yaml @@ -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 diff --git a/deploy/helm/multica/templates/_helpers.tpl b/deploy/helm/multica/templates/_helpers.tpl new file mode 100644 index 000000000..f83504c4f --- /dev/null +++ b/deploy/helm/multica/templates/_helpers.tpl @@ -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 -}} diff --git a/deploy/helm/multica/templates/backend.yaml b/deploy/helm/multica/templates/backend.yaml new file mode 100644 index 000000000..ef2b5b6c1 --- /dev/null +++ b/deploy/helm/multica/templates/backend.yaml @@ -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 }} diff --git a/deploy/helm/multica/templates/configmap.yaml b/deploy/helm/multica/templates/configmap.yaml new file mode 100644 index 000000000..9f9eb55a0 --- /dev/null +++ b/deploy/helm/multica/templates/configmap.yaml @@ -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 }} diff --git a/deploy/helm/multica/templates/frontend.yaml b/deploy/helm/multica/templates/frontend.yaml new file mode 100644 index 000000000..3eb2e9c62 --- /dev/null +++ b/deploy/helm/multica/templates/frontend.yaml @@ -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 diff --git a/deploy/helm/multica/templates/ingress.yaml b/deploy/helm/multica/templates/ingress.yaml new file mode 100644 index 000000000..ef31ada89 --- /dev/null +++ b/deploy/helm/multica/templates/ingress.yaml @@ -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 }} diff --git a/deploy/helm/multica/templates/postgres.yaml b/deploy/helm/multica/templates/postgres.yaml new file mode 100644 index 000000000..c68def4ac --- /dev/null +++ b/deploy/helm/multica/templates/postgres.yaml @@ -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 diff --git a/deploy/helm/multica/values.yaml b/deploy/helm/multica/values.yaml new file mode 100644 index 000000000..4732b6da7 --- /dev/null +++ b/deploy/helm/multica/values.yaml @@ -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 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