commit 780e25c15e68ca6303d71131cf3184995b5edd68 Author: sttlab Date: Sat May 9 06:42:45 2026 +0000 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5ae40f --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Secrets — ne jamais committer de valeurs réelles +secret.yaml +tls.yaml + +# Certificats TLS +*.crt +*.key +*.pem + +# Overrides locaux +*.local.yaml +*.override.yaml + +# Éditeurs +.vscode/ +.idea/ +*.swp +*~ + +# OS +.DS_Store +Thumbs.db +.claude \ No newline at end of file diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000..3113bfe --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,142 @@ +# LibreChat — Kubernetes Deployment + +Deploys [LibreChat](https://www.librechat.ai/) on Kubernetes with MongoDB, MeiliSearch, and the JIRA MCP server. + +## Architecture + +``` +Namespace: librechat +│ +├── Ingress nginx (librechat.sttlab.pc — TLS wildcard *.sttlab.pc) +│ └── librechat :3080 +│ ├── mongodb :27017 (StatefulSet, 5 Gi PVC) +│ └── meilisearch :7700 (Deployment, 2 Gi PVC) +│ +└── MCP JIRA (namespace: mcp) + └── mcp-jira :9000 (streamable-http, 49 tools) +``` + +| Component | Image | Storage | +|-----------|-------|---------| +| LibreChat | `ghcr.io/danny-avila/librechat:latest` | — (stateless) | +| MongoDB | `mongo:7` | 5 Gi PVC | +| MeiliSearch | `getmeili/meilisearch:v1.7` | 2 Gi PVC | + +## Directory structure + +``` +k8s/ +├── namespace.yaml Namespace definition +├── secret.yaml Credentials (gitignored — never commit real values) +├── configmap.yaml Application environment config +├── kustomization.yaml Kustomize entry point +├── mongodb/ +│ ├── pvc.yaml +│ ├── statefulset.yaml +│ └── service.yaml +├── meilisearch/ +│ ├── pvc.yaml +│ ├── deployment.yaml +│ └── service.yaml +├── librechat/ +│ ├── configmap-app.yaml Mounts librechat.yaml (MCP config, allowedDomains...) +│ ├── deployment.yaml +│ └── service.yaml +└── ingress/ + ├── ingress.yaml + └── tls.yaml TLS secret (gitignored — never commit) +``` + +## Prerequisites + +- Kubernetes cluster with an Nginx ingress controller +- A default StorageClass (for PVCs) +- `kubectl` configured against your cluster +- Wildcard TLS certificate for `*.sttlab.pc` in `$HOME/tls/` + +## Initial deployment + +### 1. Fill in secrets + +Edit `secret.yaml` with real values. Generate them with: + +```bash +openssl rand -base64 24 # MONGO_PASSWORD +openssl rand -base64 32 # MEILI_MASTER_KEY, JWT_SECRET, JWT_REFRESH_SECRET +openssl rand -hex 32 # CREDS_KEY +openssl rand -hex 8 # CREDS_IV +``` + +**Important:** `MONGO_URI` must include `?authSource=admin` because `MONGO_INITDB_ROOT_USERNAME` creates the user in the `admin` database: +``` +mongodb://librechat:@mongodb:27017/LibreChat?authSource=admin +``` + +### 2. Create the TLS secret + +```bash +kubectl apply -f k8s/namespace.yaml + +kubectl create secret tls sttlab-tls \ + --cert=$HOME/tls/sttlab.pc.crt \ + --key=$HOME/tls/sttlab.pc.key \ + --namespace=librechat \ + --dry-run=client -o yaml > k8s/ingress/tls.yaml + +kubectl apply -f k8s/ingress/tls.yaml +``` + +### 3. Deploy + +```bash +export DOMAIN=librechat.sttlab.pc +kubectl kustomize k8s/ | envsubst '${DOMAIN}' | kubectl apply -f - +``` + +> **Note:** `DOMAIN` must be exported before the pipe. An inline assignment (`DOMAIN=x cmd1 | cmd2`) does not propagate to `envsubst`. + +### 4. Verify + +```bash +kubectl get pods,ingress -n librechat +curl -sk https://librechat.sttlab.pc/health +``` + +## MCP JIRA + +The JIRA MCP server runs in the `mcp` namespace (`mcp-jira:9000`, `streamable-http` transport). + +`librechat/configmap-app.yaml` mounts `/app/librechat.yaml` into the container: + +```yaml +mcpSettings: + allowedDomains: + - "http://mcp-jira.mcp.svc.cluster.local:9000" +mcpServers: + jira: + type: streamable-http + url: http://mcp-jira.mcp.svc.cluster.local:9000/mcp +``` + +> `mcpSettings.allowedDomains` is required to allow internal k8s domains, which are blocked by default as SSRF protection. + +## Updating + +```bash +export DOMAIN=librechat.sttlab.pc +kubectl kustomize k8s/ | envsubst '${DOMAIN}' | kubectl apply -f - +``` + +Force a pod restart: + +```bash +kubectl rollout restart deployment/librechat -n librechat +``` + +## Teardown + +```bash +kubectl delete namespace librechat +``` + +> This also deletes all PVCs and their data. Back up MongoDB first if needed. diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml new file mode 100644 index 0000000..8de03e5 --- /dev/null +++ b/k8s/configmap.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: librechat-config + namespace: librechat +data: + HOST: "0.0.0.0" + PORT: "3080" + DOMAIN_CLIENT: "https://${DOMAIN}" + DOMAIN_SERVER: "https://${DOMAIN}" + MEILI_HOST: "http://meilisearch:7700" + NODE_ENV: "production" + ALLOW_EMAIL_LOGIN: "true" + ALLOW_REGISTRATION: "true" + ALLOW_SOCIAL_LOGIN: "false" + ALLOW_SOCIAL_REGISTRATION: "false" + SESSION_EXPIRY: "900000" + REFRESH_TOKEN_EXPIRY: "604800000" + DEBUG_LOGGING: "false" diff --git a/k8s/ingress/ingress.yaml b/k8s/ingress/ingress.yaml new file mode 100644 index 0000000..ed7a671 --- /dev/null +++ b/k8s/ingress/ingress.yaml @@ -0,0 +1,31 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: librechat + namespace: librechat + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: "20m" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-buffering: "off" + nginx.ingress.kubernetes.io/proxy-http-version: "1.1" + nginx.ingress.kubernetes.io/configuration-snippet: | + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; +spec: + ingressClassName: nginx + tls: + - hosts: + - librechat.sttlab.pc + secretName: sttlab-tls + rules: + - host: librechat.sttlab.pc + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: librechat + port: + number: 80 diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml new file mode 100644 index 0000000..4d628a7 --- /dev/null +++ b/k8s/kustomization.yaml @@ -0,0 +1,18 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - secret.yaml + - configmap.yaml + - mongodb/pvc.yaml + - mongodb/statefulset.yaml + - mongodb/service.yaml + - meilisearch/pvc.yaml + - meilisearch/deployment.yaml + - meilisearch/service.yaml + - librechat/configmap-app.yaml + - librechat/deployment.yaml + - librechat/service.yaml + - ingress/tls.yaml + - ingress/ingress.yaml diff --git a/k8s/librechat/configmap-app.yaml b/k8s/librechat/configmap-app.yaml new file mode 100644 index 0000000..20a8451 --- /dev/null +++ b/k8s/librechat/configmap-app.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: librechat-app-config + namespace: librechat +data: + librechat.yaml: | + version: 1.3.9 + mcpSettings: + allowedDomains: + - "http://mcp-jira.mcp.svc.cluster.local:9000" + mcpServers: + jira: + type: streamable-http + url: http://mcp-jira.mcp.svc.cluster.local:9000/mcp diff --git a/k8s/librechat/deployment.yaml b/k8s/librechat/deployment.yaml new file mode 100644 index 0000000..5912750 --- /dev/null +++ b/k8s/librechat/deployment.yaml @@ -0,0 +1,110 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: librechat + namespace: librechat +spec: + replicas: 1 + selector: + matchLabels: + app: librechat + template: + metadata: + labels: + app: librechat + spec: + initContainers: + - name: wait-for-mongodb + image: busybox:1.36 + command: + - sh + - -c + - | + until nc -z mongodb 27017; do + echo "Waiting for MongoDB..."; sleep 3; + done + - name: wait-for-meilisearch + image: busybox:1.36 + command: + - sh + - -c + - | + until nc -z meilisearch 7700; do + echo "Waiting for MeiliSearch..."; sleep 3; + done + containers: + - name: librechat + image: ghcr.io/danny-avila/librechat:latest + ports: + - containerPort: 3080 + envFrom: + - configMapRef: + name: librechat-config + env: + - name: MONGO_URI + valueFrom: + secretKeyRef: + name: librechat-secrets + key: MONGO_URI + - name: MEILI_MASTER_KEY + valueFrom: + secretKeyRef: + name: librechat-secrets + key: MEILI_MASTER_KEY + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: librechat-secrets + key: JWT_SECRET + - name: JWT_REFRESH_SECRET + valueFrom: + secretKeyRef: + name: librechat-secrets + key: JWT_REFRESH_SECRET + - name: CREDS_KEY + valueFrom: + secretKeyRef: + name: librechat-secrets + key: CREDS_KEY + - name: CREDS_IV + valueFrom: + secretKeyRef: + name: librechat-secrets + key: CREDS_IV + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: librechat-secrets + key: OPENAI_API_KEY + - name: ANTHROPIC_API_KEY + valueFrom: + secretKeyRef: + name: librechat-secrets + key: ANTHROPIC_API_KEY + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + livenessProbe: + httpGet: + path: /health + port: 3080 + initialDelaySeconds: 30 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /health + port: 3080 + initialDelaySeconds: 20 + periodSeconds: 10 + volumeMounts: + - name: app-config + mountPath: /app/librechat.yaml + subPath: librechat.yaml + volumes: + - name: app-config + configMap: + name: librechat-app-config diff --git a/k8s/librechat/service.yaml b/k8s/librechat/service.yaml new file mode 100644 index 0000000..e66d251 --- /dev/null +++ b/k8s/librechat/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: librechat + namespace: librechat +spec: + selector: + app: librechat + ports: + - port: 80 + targetPort: 3080 diff --git a/k8s/meilisearch/deployment.yaml b/k8s/meilisearch/deployment.yaml new file mode 100644 index 0000000..556ce71 --- /dev/null +++ b/k8s/meilisearch/deployment.yaml @@ -0,0 +1,56 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: meilisearch + namespace: librechat +spec: + replicas: 1 + selector: + matchLabels: + app: meilisearch + template: + metadata: + labels: + app: meilisearch + spec: + containers: + - name: meilisearch + image: getmeili/meilisearch:v1.7 + ports: + - containerPort: 7700 + env: + - name: MEILI_MASTER_KEY + valueFrom: + secretKeyRef: + name: librechat-secrets + key: MEILI_MASTER_KEY + - name: MEILI_ENV + value: production + - name: MEILI_DB_PATH + value: /meili_data + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 300m + memory: 256Mi + volumeMounts: + - name: data + mountPath: /meili_data + livenessProbe: + httpGet: + path: /health + port: 7700 + initialDelaySeconds: 20 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /health + port: 7700 + initialDelaySeconds: 10 + periodSeconds: 10 + volumes: + - name: data + persistentVolumeClaim: + claimName: meilisearch-data diff --git a/k8s/meilisearch/pvc.yaml b/k8s/meilisearch/pvc.yaml new file mode 100644 index 0000000..d96ae1b --- /dev/null +++ b/k8s/meilisearch/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: meilisearch-data + namespace: librechat +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi diff --git a/k8s/meilisearch/service.yaml b/k8s/meilisearch/service.yaml new file mode 100644 index 0000000..f0d921a --- /dev/null +++ b/k8s/meilisearch/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: meilisearch + namespace: librechat +spec: + selector: + app: meilisearch + ports: + - port: 7700 + targetPort: 7700 diff --git a/k8s/mongodb/pvc.yaml b/k8s/mongodb/pvc.yaml new file mode 100644 index 0000000..4aea42f --- /dev/null +++ b/k8s/mongodb/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mongodb-data + namespace: librechat +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi diff --git a/k8s/mongodb/service.yaml b/k8s/mongodb/service.yaml new file mode 100644 index 0000000..bb3a2a0 --- /dev/null +++ b/k8s/mongodb/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: mongodb + namespace: librechat +spec: + selector: + app: mongodb + ports: + - port: 27017 + targetPort: 27017 + clusterIP: None diff --git a/k8s/mongodb/statefulset.yaml b/k8s/mongodb/statefulset.yaml new file mode 100644 index 0000000..13df904 --- /dev/null +++ b/k8s/mongodb/statefulset.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: mongodb + namespace: librechat +spec: + serviceName: mongodb + replicas: 1 + selector: + matchLabels: + app: mongodb + template: + metadata: + labels: + app: mongodb + spec: + containers: + - name: mongodb + image: mongo:7 + ports: + - containerPort: 27017 + env: + - name: MONGO_INITDB_ROOT_USERNAME + value: librechat + - name: MONGO_INITDB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: librechat-secrets + key: MONGO_PASSWORD + - name: MONGO_INITDB_DATABASE + value: LibreChat + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + volumeMounts: + - name: data + mountPath: /data/db + livenessProbe: + exec: + command: + - mongosh + - --eval + - "db.adminCommand('ping')" + initialDelaySeconds: 30 + periodSeconds: 20 + readinessProbe: + exec: + command: + - mongosh + - --eval + - "db.adminCommand('ping')" + initialDelaySeconds: 15 + periodSeconds: 10 + volumes: + - name: data + persistentVolumeClaim: + claimName: mongodb-data diff --git a/k8s/namespace.yaml b/k8s/namespace.yaml new file mode 100644 index 0000000..f5a784d --- /dev/null +++ b/k8s/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: librechat diff --git a/k8s/secret.example.yaml b/k8s/secret.example.yaml new file mode 100644 index 0000000..113897b --- /dev/null +++ b/k8s/secret.example.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Secret +metadata: + name: librechat-secrets + namespace: librechat +type: Opaque +stringData: + MONGO_PASSWORD: "CHANGE_ME_MONGO_PASSWORD" + MONGO_URI: "mongodb://librechat:CHANGE_ME_MONGO_PASSWORD@mongodb:27017/LibreChat" + MEILI_MASTER_KEY: "CHANGE_ME_MEILI_MASTER_KEY_MIN_16_CHARS" + JWT_SECRET: "CHANGE_ME_JWT_SECRET_MIN_32_CHARS_RANDOM" + JWT_REFRESH_SECRET: "CHANGE_ME_JWT_REFRESH_SECRET_MIN_32_CHARS" + CREDS_KEY: "CHANGE_ME_32_BYTE_HEX_KEY_0000000000000000000000000000" + CREDS_IV: "CHANGE_ME_16BYTE" + # Optional: fill in your AI provider API keys + OPENAI_API_KEY: "" + ANTHROPIC_API_KEY: ""