# mem0 自托管部署 ## 镜像 ``` ccr.ccs.tencentyun.com/tei_agent/mem0:latest ``` ## 依赖服务(tei namespace) | 服务 | 地址 | 用途 | |---|---|---| | PostgreSQL | 192.168.3.49:5432 | 关系数据存储 | | Qdrant | qdrant:6333 | 向量存储 | | TEI (BGE-M3) | tei:8080 | Embedder(文本→向量) | ## 环境变量 ### ConfigMap (mem0-env) | 变量 | 值 | 说明 | |---|---|---| | APP_DB_NAME | mem0 | 数据库名 | | QDRANT_HOST | qdrant | Qdrant 服务地址 | | QDRANT_PORT | 6333 | Qdrant 端口 | | QDRANT_COLLECTION_NAME | mem0 | 集合名 | | EMBEDDER_PROVIDER | tei | Embedder 使用 TEI | | TEI_ENDPOINT | http://tei:8080 | TEI endpoint | | LLM_PROVIDER | openai | LLM provider(MiniMax 兼容 OpenAI 格式) | | OPENAI_API_KEY | YOUR_MINIMAX_KEY | MiniMax API key | | OPENAI_BASE_URL | https://api.minimax.chat/v1 | MiniMax API 地址 | | AUTH_DISABLED | false | 启用认证 | | MEM0_TELEMETRY | false | 关闭遥测 | | REQUEST_LOG_RETENTION_DAYS | 30 | 日志保留天数 | | HISTORY_DB_PATH | /app/data/mem0_history.db | SQLite 历史数据库路径 | ### Secret (mem0-secrets) | 变量 | 说明 | |---|---| | JWT_SECRET | JWT 签名密钥 | | ADMIN_API_KEY | 管理后台 API key | | POSTGRES_PASSWORD | PostgreSQL 密码 | ## 数据库迁移 (Alembic) mem0 server 使用 Alembic 管理 PostgreSQL schema。**首次部署前必须先执行迁移**,创建 `request_logs` 等表。 ### 迁移 Job ```yaml apiVersion: batch/v1 kind: Job metadata: name: mem0-migrate namespace: tei spec: ttlSecondsAfterFinished: 300 # 完成后5分钟自动清理 template: spec: restartPolicy: OnFailure containers: - name: alembic image: ccr.ccs.tencentyun.com/tei_agent/mem0:latest command: ["alembic", "upgrade", "head"] envFrom: - configMapRef: name: mem0-env - secretRef: name: mem0-secrets env: - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: dify-prod-db-secret key: password resources: limits: memory: "512Mi" cpu: "500m" ``` ### 迁移部署步骤 ```bash # 1. 先跑迁移(只执行一次) kubectl apply -f mem0-migrate-job.yaml # 2. 确认迁移完成 kubectl get job mem0-migrate -n tei -w # 3. 确认成功后再部署 mem0 kubectl apply -f mem0-deployment.yaml # 4. 如果迁移失败,查看原因 kubectl logs job/mem0-migrate -n tei ``` **重要**:迁移 Job 只跑一次。Pod 重启时不需要重新迁移,PostgreSQL schema 不会天天变。 ## 前置要求 1. **pgvector 扩展** — PostgreSQL 需要安装 pgvector 2. **mem0 数据库** — 需要提前创建 3. **mem0-migrate Job** — 首次部署前必须先执行迁移 4. **mem0-history PVC** — 必须提前创建 5. **Qdrant collection** — mem0 启动时自动创建(首次调用时) ## 部署清单 ### PVC + ConfigMap + Secret + Deployment + Service ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mem0-history namespace: tei spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi storageClassName: standard # 根据你的 StorageClass 调整 --- apiVersion: v1 kind: ConfigMap metadata: name: mem0-env namespace: tei data: APP_DB_NAME: "mem0" QDRANT_HOST: "qdrant" QDRANT_PORT: "6333" QDRANT_COLLECTION_NAME: "mem0" EMBEDDER_PROVIDER: "tei" TEI_ENDPOINT: "http://tei:8080" LLM_PROVIDER: "openai" OPENAI_API_KEY: "YOUR_MINIMAX_KEY" OPENAI_BASE_URL: "https://api.minimax.chat/v1" AUTH_DISABLED: "false" MEM0_TELEMETRY: "false" REQUEST_LOG_RETENTION_DAYS: "30" HISTORY_DB_PATH: "/app/data/mem0_history.db" --- apiVersion: v1 kind: Secret metadata: name: mem0-secrets namespace: tei type: Opaque stringData: JWT_SECRET: "your-jwt-secret-change-me" ADMIN_API_KEY: "your-admin-key-change-me" POSTGRES_PASSWORD: "gitlab" --- apiVersion: apps/v1 kind: Deployment metadata: name: mem0 namespace: tei spec: replicas: 1 selector: matchLabels: app: mem0 template: metadata: labels: app: mem0 spec: containers: - name: mem0 image: ccr.ccs.tencentyun.com/tei_agent/mem0:latest ports: - containerPort: 8000 name: http envFrom: - configMapRef: name: mem0-env - secretRef: name: mem0-secrets volumeMounts: - name: mem0-history mountPath: /app/data resources: limits: cpu: "2" memory: "4Gi" requests: cpu: "500m" memory: "1Gi" volumes: - name: mem0-history persistentVolumeClaim: claimName: mem0-history --- apiVersion: v1 kind: Service metadata: name: mem0 namespace: tei spec: ports: - port: 8000 name: http selector: app: mem0 ``` ## 验证 ```bash # 检查 PVC kubectl get pvc -n tei mem0-history # 检查 Pod kubectl get pods -n tei -l app=mem0 # 查看日志 kubectl logs -n tei -l app=mem0 --tail=50 # 健康检查 curl http://mem0:8000/health ``` ## History SQLite 说明 mem0 的对话历史存储在 SQLite,位于 `/app/data/mem0_history.db`。**必须用 PVC 持久化**,emptyDir 挂载重启后数据丢失。 如果 PVC 空间不足或不需要历史功能,可以改用空目录(数据丢失但不影响核心记忆功能): ```yaml # 测试/轻量级环境用 emptyDir volumes: - name: mem0-history emptyDir: {} ``` ## Dashboard 部署 mem0 dashboard 是 Next.js 应用,需要单独部署。 ### 环境变量 ```yaml NEXT_PUBLIC_API_URL=http://mem0.tei.svc.cluster.local:8000 # 浏览器调用(通过 Ingress) API_INTERNAL_URL=http://mem0.tei.svc.cluster.local:8000 # 服务端内部调用(K8s 内部直连) NEXT_PUBLIC_INSTANCE_NAME=Mem0 ``` **`API_INTERNAL_URL`**:K8s 内部服务间通信,直接使用 Service DNS,不需要暴露到外部。浏览器无法解析 `mem0.tei.svc.cluster.local`,所以前端用 `NEXT_PUBLIC_API_URL` 通过 Ingress 访问。 两者可以相同,但分离部署时: - `NEXT_PUBLIC_API_URL` = 对外域名(Ingress) - `API_INTERNAL_URL` = 集群内部 `mem0.tei.svc.cluster.local:8000` ### CORS 配置 mem0 server 使用 CORSMiddleware,需要通过环境变量配置允许的来源: ```yaml DASHBOARD_URL=https://mem0.your-domain.com # dashboard 的外部访问地址 ``` mem0 server 启动时读取 `DASHBOARD_URL`,设置 `allow_origins=[DASHBOARD_URL]`。 **常见问题**: - 浏览器访问 dashboard 登录页时,返回 `400 Bad Request` - OPTIONS 预检请求失败,因为 `allow_origins` 为空 - Swagger 能正常登录(同域请求,无 CORS 问题) 确保 `DASHBOARD_URL` 设为 dashboard 的外部访问地址(与 `NEXT_PUBLIC_API_URL` 的域名部分一致)。 ## sentence_transformers 导入错误(永久修复) mem0 的 `huggingface.py` 在文件顶部执行 `from sentence_transformers import SentenceTransformer`,即使走 `huggingface_base_url` 路径(用 OpenAI 客户端调用 TEI,不需要本地模型)也会尝试加载,造成 `ModuleNotFoundError`。 通过 init container + emptyDir 挂载实现持久修复,pod recreate 不丢失: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: mem0 namespace: tei spec: replicas: 1 selector: matchLabels: app: mem0 template: metadata: labels: app: mem0 spec: initContainers: - name: patch-mem0 image: ccr.ccs.tencentyun.com/tei_agent/mem0:latest command: - sh - -c - | cp -r /usr/local/lib/python3.12/site-packages/mem0 /tmp/mem0-patch/ sed -i 's/from sentence_transformers import SentenceTransformer/# from sentence_transformers import SentenceTransformer/' /tmp/mem0-patch/embeddings/huggingface.py echo "patch done" volumeMounts: - name: mem0-code mountPath: /tmp/mem0-patch containers: - name: mem0-ui image: ccr.ccs.tencentyun.com/tei_agent/mem0-ui:latest ports: - containerPort: 3000 name: http env: - name: NEXT_PUBLIC_API_URL value: "https://api.mem0.violin-work.online" - name: API_INTERNAL_URL value: "http://mem0.tei.svc.cluster.local:8000" - name: NEXT_PUBLIC_INSTANCE_NAME value: Mem0 resources: limits: cpu: 200m memory: 500Mi requests: cpu: 100m memory: 250Mi - name: mem0 image: ccr.ccs.tencentyun.com/tei_agent/mem0:latest ports: - containerPort: 8000 name: http envFrom: - configMapRef: name: mem0-env - secretRef: name: mem0-secrets env: - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: dify-prod-db-secret key: password volumeMounts: - name: mem0-history mountPath: /app/data - name: mem0-code mountPath: /usr/local/lib/python3.12/site-packages/mem0 resources: limits: cpu: "2" memory: 4Gi requests: cpu: "500m" memory: 1Gi volumes: - name: mem0-history persistentVolumeClaim: claimName: mem0-history - name: mem0-code emptyDir: {} ``` **原理**:init container 先于主容器启动,把镜像里的 mem0 代码复制到 emptyDir,sed 补丁打在 emptyDir 里。主容器挂载 emptyDir 到 site-packages,覆盖镜像原有文件,实现补丁持久化(recreate 不丢)。 ## 功能测试 mem0 未为 K8s 做适配,无健康检查端点。手动测试核心 API: ```bash cat > /tmp/test_mem0.py << 'TESTEOF' import urllib.request, json, uuid BASE = "http://mem0:8000" def req(method, path, data=None): url = BASE + path body = json.dumps(data).encode() if data else None headers = {"Content-Type": "application/json"} try: r = urllib.request.urlopen(urllib.request.Request(url, data=body, headers=headers, method=method), timeout=10) return json.loads(r.read()), r.status except urllib.error.HTTPError as e: return json.loads(e.read()), e.code except Exception as e: return str(e), 0 uid = str(uuid.uuid4()) print("=== 1. 创建用户 ===") print(req("POST", "/api/v1/users", {"user_id": uid, "email": f"{uid}@test.com"})) print("=== 2. 添加记忆 ===") print(req("POST", "/api/v1/memories", {"text": "我叫张三,我喜欢Python", "user_id": uid})) print("=== 3. 搜索记忆 ===") print(req("GET", f"/api/v1/memories?query=python&user_id={uid}")) print("=== 4. 获取历史 ===") print(req("GET", f"/api/v1/history?user_id={uid}")) TESTEOF kubectl exec -n tei deploy/mem0 -- python3 /tmp/test_mem0.py ```