mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
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:
175
SELF_HOSTING.md
175
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://<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:
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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` 有问题
|
||||
|
||||
16
deploy/helm/multica/Chart.yaml
Normal file
16
deploy/helm/multica/Chart.yaml
Normal 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
|
||||
35
deploy/helm/multica/templates/_helpers.tpl
Normal file
35
deploy/helm/multica/templates/_helpers.tpl
Normal 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 -}}
|
||||
143
deploy/helm/multica/templates/backend.yaml
Normal file
143
deploy/helm/multica/templates/backend.yaml
Normal 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 }}
|
||||
29
deploy/helm/multica/templates/configmap.yaml
Normal file
29
deploy/helm/multica/templates/configmap.yaml
Normal 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 }}
|
||||
52
deploy/helm/multica/templates/frontend.yaml
Normal file
52
deploy/helm/multica/templates/frontend.yaml
Normal 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
|
||||
59
deploy/helm/multica/templates/ingress.yaml
Normal file
59
deploy/helm/multica/templates/ingress.yaml
Normal 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 }}
|
||||
99
deploy/helm/multica/templates/postgres.yaml
Normal file
99
deploy/helm/multica/templates/postgres.yaml
Normal 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
|
||||
127
deploy/helm/multica/values.yaml
Normal file
127
deploy/helm/multica/values.yaml
Normal 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
|
||||
Reference in New Issue
Block a user