# 部署(生产 · openclaw-01) > 适用于当前唯一的线上环境:`openclaw-01`(192.168.8.188),访问入口 `http://192.168.8.188:8200`。 > 本地开发见 [`DEVELOPMENT.md`](DEVELOPMENT.md)。部署铁律见仓库根 [`AGENTS.md`](../AGENTS.md) 第 2 节。 ## 1. 生产拓扑 ``` 浏览器 ──HTTP──▶ openclaw-01:8200 └ gunicorn(2 × UvicornWorker) ── app.main:app ├ /api/v1/** 业务 API └ / 前端产物 web/dist(单页应用,404 回退 index.html) │ ┌────────────────────────┼────────────────────────┐ ▼ ▼ ▼ fnsvr-01:8432 PG 17.6 fnsvr-01:6379 Redis 本机 backend/var/uploads 库 dadaotask_prod db 6(独立库号) 附件文件 角色 dadao_prod_app(RLS) 角色 dadao_prod_owner(迁移) ``` 为什么是这个形状: - **生产库和开发库在同一个 PG 实例**(fnsvr-01),所以生产用**独立的库 + 独立的角色**。复用 `dadao_app` 等于拿到开发口令就能连生产。迁移里的 `GRANT`/`DEFAULT PRIVILEGES` 按 `APP_DB_ROLE` / `OWNER_DB_ROLE` 生成,靠 `backend/app/roles.py` 注入。 - **Redis 用 6 号库**隔离:共享实例没有密码,靠库号避免和别的项目撞键(验证码、幂等键)。 - **没有 nginx**:openclaw 账号无 sudo 密码,改不了系统 nginx,所以后端单端口同时托管 API 与前端产物(同源,免 CORS)。root 愿意配合时按 `deploy/prod/nginx.dadaotask.conf.example` 加反代,不用改应用。 - **用户级 systemd**:openclaw-01 上所有服务(easymemo、portal-site…)都是 `systemctl --user` 起的,本方案沿用,全程不需要 root。 ## 2. 首次部署(一次性) 前置:能 SSH 到 openclaw-01;fnsvr-01 的 root 权限(只在建库这一步需要)。 ### 2.1 建生产库与角色(在 fnsvr-01 上,root) ```bash # 口令自己生成,别用示例里的 OWNER_PW='' APP_PW='' \ bash deploy/prod/provision-db.sh ``` 会创建:库 `dadaotask_prod`(owner `dadao_prod_owner`)、角色 `dadao_prod_owner` / `dadao_prod_app`(`NOSUPERUSER NOBYPASSRLS`,否则 RLS 会被跳过)、`public` schema 归 owner、`REVOKE CREATE ... FROM PUBLIC`。建表与 RLS 策略由 alembic 负责(2.4)。 ### 2.2 代码落地(在 openclaw-01 上,openclaw 身份) ```bash git clone --branch main --single-branch http://gitea.tatta.cn/Tatta/DadaoTask.git ~/app/dadaotask ``` ### 2.3 配置(不进仓库) ```bash mkdir -p ~/.config/dadaotask && chmod 700 ~/.config/dadaotask cp ~/app/dadaotask/deploy/prod/env.production.example ~/.config/dadaotask/env.production chmod 600 ~/.config/dadaotask/env.production vim ~/.config/dadaotask/env.production # 替换全部 __CHANGE_ME__ ``` 必填项:`DATABASE_URL` / `MIGRATION_DATABASE_URL`(2.1 的口令)、`JWT_SECRET`(≥32 位随机串)、`REDIS_URL`、`WEB_DIST_DIR`、`UPLOAD_DIR`。 两个踩过的坑: - **`CORS_ORIGINS` 必须写成 JSON 数组**(`'["http://...","http://..."]'`)。它是 list 字段,逗号分隔的字符串会让 pydantic-settings 当 JSON 解析并抛 `SettingsError`,alembic 这一步就挂。 - **Redis 有口令**:fnsvr-01 的 redis 开了 `requirepass`(口令看 `docker inspect redis`),写进 URL 时要百分号编码(`#` → `%23`),否则连不上、发码/登录直接 500。 **当前状态是内测模式**:短信通道还没接(非 dev provider 直接 501),所以 `SMS_PROVIDER=dev` + `INTERNAL_TEST_LOGIN=true`,固定验证码 `123456`。启动日志会打 WARNING。**只允许内网访问**;短信通道接好后删掉 `INTERNAL_TEST_LOGIN` 并把 `SMS_PROVIDER` 换成真实供应商。 ### 2.4 装服务单元 ```bash mkdir -p ~/.config/systemd/user cp ~/app/dadaotask/deploy/prod/dadaotask-api.service ~/.config/systemd/user/ systemctl --user daemon-reload ``` ### 2.5 首次上线 ```bash bash ~/app/dadaotask/deploy/prod/deploy.sh ``` ## 3. 日常发布(每次上线) ```bash # 本地:提交并 push 到 main(生产只跑仓库 HEAD) # 服务器: bash ~/app/dadaotask/deploy/prod/deploy.sh ``` 脚本做六件事,任何一步失败即中止:代码对齐 `origin/main`(`git reset --hard`,禁止服务器手工改)→ 装 Python 依赖 → `alembic upgrade head`(用迁移角色)→ **在服务器上** `npm ci && npm run build` → 写 `env.build` 并重启 → 校验 `/health` 与 `/version` 的 commit 等于 HEAD。 前端产物只在服务器上构建,**不允许**本地上传 dist(AGENTS.md 铁律 3)。 ## 4. 部署后校验 ```bash curl -s http://192.168.8.188:8200/health # {"status":"ok","db":true,...} curl -s http://192.168.8.188:8200/version # commit 必须等于 git rev-parse --short HEAD curl -sI http://192.168.8.188:8200/ | head -1 # 200,text/html(前端产物) ``` 浏览器验收:打开 `http://192.168.8.188:8200` → 手机号任意 + 验证码 `123456` 登录 → 任务/工单/知识各点一遍。 ## 5. 运维 ```bash systemctl --user status dadaotask-api # 状态 systemctl --user restart dadaotask-api # 重启 journalctl --user -u dadaotask-api -f # 日志(gunicorn 输出到 stdout) ``` - 附件落盘:`backend/var/uploads`(按工作区分子目录),删库不删文件,备份时一起打包 - 回滚:`cd ~/app/dadaotask && git reset --hard <旧 commit> && bash deploy/prod/deploy.sh`;**数据库迁移不回滚**,回滚代码前确认旧代码兼容新表结构 - 端口:`8200`(systemd 单元与 `deploy.sh` 的 `PORT` 默认值,两处要一致) ## 6. 已知限制(当前阶段) | 限制 | 影响 | 解除方式 | |---|---|---| | 没有 HTTPS / 域名 | 只能内网 IP 访问,令牌明文传输 | 域名 + nginx + 证书(需 root) | | 内测固定验证码 | 谁都能用任意手机号登录 | 接短信通道,关 `INTERNAL_TEST_LOGIN` | | 无 pgvector | 语义检索走 `real[]` 全表扫描,数据量大了会慢 | 上独立 pgvector 实例(`deploy/docker-compose.vector.yml`) | | 上传目录在本机磁盘 | 机器坏了附件就没了 | 备份 `backend/var/uploads`,或将来到对象存储 | | 共享 PG / Redis | 别人占满连接或内存会波及本站 | 独立实例 |