# 生产部署环境与上线流程 > 状态:✅ 当前有效。2026-08-27 首次整理,来源于登录服务器实地核查(非猜测)。 ## 服务器 - 地址:`root@60.205.214.169`(hostname `hexyun`)。本机已配好免密 SSH,直接 `ssh root@60.205.214.169` 即可,不需要额外问用户要密码/密钥。 - 对外域名 `lemonboss.wudaima.com` 由这台机器提供服务。 - 同一台机器上还跑着大量不相关的容器(wordpress、milvus、rasa、chroma 等),`docker` 相关命令务必认准容器名 `lemonboss`,不要用 `docker restart` 之类命令波及别的服务。 ## 部署拓扑 - **代码目录**(host 侧):`/opt/lemonboss`,属主 `hexyun:hexyun`。是一个独立 git 仓库,`origin` 与本地仓库一致,指向 `git@gitlab-wuwei.hexpm.com:yibo/lemonboss.git`,跟踪分支 `main`。 - **运行方式**:Docker 容器 `lemonboss`(镜像 `lemonboss-base:latest`),重启策略 `unless-stopped`。 - `/opt/lemonboss` bind-mount 到容器内 `/app` - `/opt/data` bind-mount 到容器内 `/data` - entrypoint:`docker-entrypoint.sh` → `node server/index.js` - **端口**:容器内 3010 → 宿主机 `127.0.0.1:3011`(对外走反向代理到 `lemonboss.wudaima.com`,代理具体配置未查)。 ## 更新上线流程 1. 本地正常提交,`git push origin main`。 2. 登录服务器:`ssh root@60.205.214.169`,`cd /opt/lemonboss && git pull origin main`。 - **必须用 `root` 拉代码**:目录属主虽是 `hexyun`,但只有 `root` 配了到 `gitlab-wuwei.hexpm.com` 的部署 SSH key;`hexyun` 没配任何 key,`sudo -u hexyun git pull` 会直接因 host key / 鉴权失败报错。 - `git pull` 完成后,把本次改动涉及的文件 `chown hexyun:hexyun` 改回属主(`git pull` 用 root 跑,新拉下来的文件会变成 `root:root`),避免以后应用进程(跑在 `hexyun` 身份下)对这些文件失去写权限。整个目录不建议全量 `chown -R`(node_modules 体量大且没必要)。 3. `docker restart lemonboss` 让新代码生效——bind mount 是实时的,但 Node 进程不会自动热重载,必须重启容器内进程。 4. 验证: - `curl http://127.0.0.1:3011/api/health` 应返回 200 - `docker logs --tail 40 lemonboss` 确认启动日志无异常(正常应看到 `✅ 服务启动: http://0.0.0.0:3010` 等心跳/看门狗初始化行) ## ⚠️ 环境变量是固化在容器里的,改 `.env` + `docker restart` 不会生效 2026-08-27 踩过一次坑:`ANTHROPIC_BASE_URL` / `ANTHROPIC_API_KEY` / `ANTHROPIC_MODEL`(决定 Claude CLI 实际打到哪个模型厂商)这几个变量,是当初 `docker run -e ...` 时**直接固化进容器配置**的(`docker inspect lemonboss --format '{{.Config.Env}}'` 能看到),跟 bind-mount 进去的 `/opt/lemonboss/.env` 文件是两码事——Node 进程虽然也会 `dotenv` 加载 `.env`,但容器注入的 env 已经先一步存在于 `process.env`,dotenv 默认不覆盖已存在的变量,所以改 `.env` 完全不生效,`docker restart` 只是重启同一个容器进程,也不会重新读取新的 `-e` 值。 **要真正改这类模型/密钥配置,必须重建容器**(`docker restart` 无效): ```bash # 1. 保留旧容器做回滚,不要直接删 TS=$(date +%Y%m%d%H%M%S) docker rename lemonboss lemonboss-old-$TS docker stop lemonboss-old-$TS # 2. 用完全相同的镜像/端口/挂载/重启策略/JWT_SECRET 重建,只改要换的那几个 -e docker run -d --name lemonboss \ --restart unless-stopped \ --user 1000:1000 \ -p 127.0.0.1:3011:3010 \ -v /opt/lemonboss:/app \ -v /opt/data:/data \ -e NODE_ENV=production -e PORT=3010 -e HOST=0.0.0.0 \ -e CORS_ORIGIN=https://lemonboss.wudaima.com \ -e JWT_SECRET=<原样抄一份,改错会让所有已登录用户掉线> \ -e ANTHROPIC_BASE_URL=... -e ANTHROPIC_API_KEY=... -e ANTHROPIC_MODEL=... \ lemonboss-base:latest # 3. 验证:health 200 + docker exec 进去看 /proc//environ 确认新值生效 + 跑一次真实 invoke ``` 完整现有配置可以先 `docker inspect lemonboss > backup.json` 存一份再动手,照抄里面的 `Env`/`Mounts`/`HostConfig.PortBindings`/`Config.User` 等字段,一个都不能漏,尤其是 `JWT_SECRET`。 **当前模型配置**(2026-08-27 起):智谱 GLM-5.3-Flash,`ANTHROPIC_BASE_URL=https://open.bigmodel.cn/api/anthropic`,`ANTHROPIC_MODEL=glm-5.3-flash`(此前是 DeepSeek `deepseek-v4-flash` + `https://api.deepseek.com/anthropic`)。 ## 跨环境同步公司数据(graph.sqlite) 2026-08-28 把本地 `mycompany` / `innovator-fde-trial` 两家公司同步到生产时踩出来的经验。 「公司数据」分两半,**必须分开处理**: 1. **工作文件**:`data/companies/{cid}/`(memory / conversations / workspace 等),普通文件,直接 `rsync -az` 到 `/opt/lemonboss/data/companies/{cid}/`,传完 `chown hexyun:hexyun`(root 传的文件 默认 root:root)。 2. **注册信息**:「这家公司雇了谁、org/work graph 长什么样、goal/task/run 记录」全部在 `data/graph.sqlite` 里,不在文件系统里——只搬文件,网页上完全看不到这家公司。 第 2 部分**不要手写 SELECT * 去搬**,用 `scripts/sync-company-to-graph-db.js`(先 `--dry-run` 看 一遍行数,确认无误再去掉这个 flag 跑真的)。这个脚本已经绕开两个手搓时踩过的坑: - **列序不能信**:本地库和生产库的表字段名一样,但物理列序可能不同——一个是老库一路 `ALTER TABLE` 攒出来的顺序,一个是某次重建/清空后吃到的当时最新 `CREATE TABLE` 字面量顺序。 `SELECT *` 位置对齐的 `INSERT` 会把值静默写进错误的列,`NOT NULL` 列能报错拦一部分,可空/ 同类型的列错了不会有任何提示。详见 `server/services/graph-db.js` 的 `migrate()` 函数头注释。 - **`graph_runs` ⇄ `graph_run_nodes` 是循环外键**(`graph_runs.parent_node_run_id` 指 `graph_run_nodes(id)`,`graph_run_nodes.run_id` 指 `graph_runs(id)`),批量搬迁需要先插 `graph_runs`(`parent_node_run_id` 置空)、再插 `graph_run_nodes`、最后回填。**回填那条 `UPDATE` 的关联子查询必须给内外表都加别名**——两边都用裸表名的话,SQLite 会把子查询里的列 解析到错误的关联对象,静默回填成 `NULL`,`changes` 还显示"成功",不会报错。 操作顺序:先在生产上备份(`tar` 打包 `companies/` 目录 + 用 `db.backup()` 存一份 `graph.sqlite.bak-`),再 rsync 文件,再跑同步脚本(对着生产的 `/app/data/graph.sqlite`, 可以通过 `docker exec lemonboss node scripts/sync-company-to-graph-db.js ...` 或者先 `docker cp` 脚本进去跑),最后 `docker restart lemonboss` 让心跳/graph-seed 重新装载。 ## 其他线索 - `/opt/lemonboss/.env` 里的 `OPEN_API_KEYS` 是 `/api/open/*` 外部调用的鉴权 key,本文档不记录明文值。**注意**:`.env` 文件本身对 `ANTHROPIC_*` 这三个变量是摆设(见上),但 `OPEN_API_KEYS` 等其他没有被 `docker run -e` 固化的变量,还是照常从 `.env` 里读取生效的——只有被固化过的那几个例外。 - `/opt/lemonboss/.data/jobs-openapi.json` 是 open-api invoke 端点(`server/routes/open-api.js`)的 job 记录,容器重启时会从这个文件恢复历史 job(日志里 `[JobManager] Restored N jobs` 就是这个)——这是 job 记录,不是会话历史,二者存储位置不同(会话历史见 `system/agents/{agentId}/{companyId|_global}/conversations/`)。