15 KiB
| name | description | category | version |
|---|---|---|---|
| webapp-deploy | 部署 Web 应用(应用脚手架部署到测试/生产机)时必读——build.sh 标准、目录结构、测试环境、venv、git 仓库管理。不加载会漏 build.sh 标准步骤、部署目录不规范。业务模块开发用 module-development-spec,部署环境信息从 env/{test,prod}.json 读。 | devops | 1.0.0 |
Web 应用部署规范
核心原则
- 所有代码提交到远端仓库,测试环境只能通过 build.sh 部署
- 所有模块从 git clone 到 pkgs/,禁止手动复制源码
- 禁止在测试环境直接修改代码,必须在本地开发 → git push → 测试环境 git pull + build.sh
- 应用有独立 venv,不共享其他应用的 Python 环境
- 部署前先确认运行进程的真实工作目录:
readlink /proc/$(pgrep -f <app>.py | head -1)/cwd。同一台服务器上可能存在同一应用的多份 checkout(不同用户/不同目录,home 软链也可能误导),git pull 和重启必须落在运行进程实际加载代码的那份上,否则"部署成功"但行为完全不变(2026-08 ragserver 事故:在 apitest 的 ~/ragserver 上验证"HEAD=修复版",实际进程跑的是 rag 用户 /d/rag/ragserver 的旧代码)
目录结构
~/<app>/ # 应用根目录
├── app/ # 入口代码
├── build.sh # 一键构建脚本
├── start.sh / stop.sh # 启停
├── conf/config.json # 应用配置
├── pkgs/ # 所有依赖模块 clone 到此
│ ├── apppublic/ # git clone
│ ├── sqlor/
│ ├── ahserver/
│ ├── rbac/
│ ├── bricks/
│ ├── <业务模块>/ # git clone
│ └── ...
├── py3/ # 独立 venv
├── wwwroot/ # 前端文件
├── logs/
├── files/
└── scripts/
build.sh 标准流程
- 硬性约束:build.sh 必须 fail-fast——脚本开头
set -e(或每步显式检查返回码),任何一步失败(建表/插种子/pip install/xls2ui)立即exit 1终止,禁止报错后继续执行后续步骤或假装成功。特别:init_data.py 插种子失败(JSON 解析错误/表缺失)时,若 build.sh 仍继续start app,会导致应用带缺表/缺种子启动、/healthz 失败,且部署日志里同时出现报错和「done」造成「看似成功」的假象。build.sh 末尾的 start 必须只在前面所有步骤都成功后才执行。
- 创建独立 venv:
python3 -m venv py3 - build.sh 创建
pkgs/目录并cd pkgs,将所有模块 git clone 进来:- 基础共享包:apppublic, sqlor, ahserver, rbac, xls2ddl, appbase, bricks-for-python, bricks
- 本项目业务模块:必须先提交到远端仓库再 clone,禁止 mv/复制源码进 pkgs/(模块开发完成即须推送远端,部署端只认远端仓库)
- 构建 bricks 前端:
cd pkgs/bricks/bricks && bash build.sh(生成 ../dist)→ 应用目录ln -sf ../pkgs/bricks/dist wwwroot/bricks pip install所有 pkgs/ 模块(xls2ddl 提供 json2ddl/xls2ui/dbloader 三个 CLI,装进 venv 后${VENV}/bin/下可用)- 逐模块执行四步安装(见下节「模块安装四步」,每个业务模块都走一遍,顺序不可跳)
- 模块 wwwroot 软链接(非 cp,四步③已含):
ln -sf ../pkgs/$mod/wwwroot wwwroot/$mod - 下划线→连字符软链接:
ln -sf pipeline-sdlc wwwroot/pipeline_sdlc - 修复 created_by: sed 注入
ns['created_by'] = userorgid - 创建 runtime 目录
模块安装四步(每个业务模块必走,一键部署代码必须包含)
模块代码到位后,逐个模块按以下四步安装,任何一步失败即终止(配合上方 fail-fast 约束):
MOD=<模块名> # 如 world
# ① 建表:进 models 执行 json2ddl 生成 DDL → 建表
cd "pkgs/${MOD}/models"
"${VENV}/bin/json2ddl" mysql . > "/tmp/${MOD}_ddl.sql"
mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASSWORD}" "${DB_NAME}" < "/tmp/${MOD}_ddl.sql"
cd - >/dev/null
# ② CRUD 页面:进 json 执行 build.sh(标准内容一行:
# xls2ui -m ../models -o ../wwwroot <模块名> *.json)
cd "pkgs/${MOD}/json"
bash build.sh
cd - >/dev/null
# ③ wwwroot 软链(非 cp):ln -sf ../pkgs/${MOD}/wwwroot wwwroot/${MOD}
ln -sf "../pkgs/${MOD}/wwwroot" "wwwroot/${MOD}"
# ④ 初始化数据:模块有 data/*.xlsx 时用 dbloader 导入(无则跳过)
if [ -d "pkgs/${MOD}/data" ] && ls pkgs/${MOD}/data/*.xlsx >/dev/null 2>&1; then
cd "pkgs/${MOD}/data"
for f in *.xlsx; do "${VENV}/bin/dbloader" "${DEPLOY_DIR}" "${DB_NAME}" "$f"; done
cd - >/dev/null
fi
要点:
- ①先于②:xls2ui 生成页面时会读 models 定义,建表失败不拦住会让应用带缺表启动
- ②的 json/build.sh 是模块仓库自带文件(标准内容就一行
xls2ui -m ../models -o ../wwwroot <模块名> *.json),模块开发时就必须生成它,部署端只执行不现编 - ④的 dbloader 参数:
dbloader <应用部署目录> <库名> <xlsx文件>,在模块 data/ 目录下逐个执行 - 基础框架表(users/organization/role/permission 等)不在此四步内——它们在应用自己的
scripts/ddl.sql里一次建好(全新库必备,否则 rbac 查询报错)
部署前置
- 手工创建应用独立数据库(如
CREATE DATABASE pipeline CHARACTER SET utf8mb4) - 写入
conf/config.json的databases.<app_db>配置 sage库仅用于 rbac/session 共享服务
config.json 配置
SAGE_RBAC_DB: 直接设为应用的数据库名(如rag),不通过module_dbname映射databases: 应用独立库password_key: 应用自身的加密密钥(独立应用不共享 Sage key)- session 持久化需要 Redis 配置: 无
session_max_time和session_issue_time时 session 仅存内存,重启即丢失用户登录态
"session_max_time": 3000,
"session_issue_time": 2500,
"session_redis": {"host": "127.0.0.1", "port": 6379, "db": 1}
- 不设
module_dbname:get_module_dbname(m)全部返回应用数据库名 - DB 密码必须 AES 加密 — sqlor 的
SQLor.__init__始终调用aes_decode_b64(key, password),明文密码会抛binascii.Error: Incorrect padding。用aes_encode_b64(password_key, plaintext)生成密文再写入 config - sqlor 配置细节 → 详见
references/sqlor-config-pitfalls.md - RBAC 表迁移 → 详见
references/rbac-migration.md
初始化
source py3/bin/activate
python scripts/load_path.py # RBAC 权限注册
python scripts/init_data.py # 初始数据(appcodes 等)
部署验证(必须)
部署后不可只检查语法或 API 返回码即宣布成功:
- 验证必须针对运行进程实际加载的那份 checkout(先
readlink /proc/<pid>/cwd确认),不要在任何"看起来像"的目录上验证后就声称部署成功——子代理报告"HEAD=修复版"不可直接采信,hub 必须亲自在运行目录复核 - curl 逐路径测 HTTP 码
- 浏览器登录后逐菜单点击,确认每个页面正常加载
- 新增/编辑表单每个字段测试可操作性
- 下拉选项确认显示正确文本(非 undefined/乱码)
- 验证权限:登录用户应能访问所有已注册路径
- 修 bug 类部署:用触发 bug 的完整请求端到端复测(登录→参数→断言返回内容),而不是只测 HTTP 200
- 用户仍报 bug 时,先确认他看到的是不是已部署版本:grep 用户页面文案里的字面量(按钮文字、提示语)在服务器 .ui/.js 文件中是否存在。grep 不到 → 用户浏览器加载的是缓存旧前端或另一份部署,后端再修也没用(2026-08 ragserver:用户页面有 "Search/Clear/drop in or click to choose file" 文案,而已部署 search.ui 里完全没有这些文本 → 旧缓存 UI)
- 后端 curl 正常但用户页面异常:dspy 顶部加
info(params_kw)日志(无需 import),部署重启后让用户复现一次再 grep 日志。用户操作完全没进日志 → 请求没到这个实例:cat /etc/nginx/sites-enabled/<app>看 proxy_pass 端口 vs 运行进程监听端口(ss -ltnp)。同机多个相似应用监听不同端口时(ragserver:9180 别的应用,域名实际转 9181),curl 本地端口正常 ≠ 域名链路正常。
测试环境
- SSH 到测试服务器
- 应用源码在
~/<app>/ - 执行
git pull && bash build.sh && ./stop.sh && ./start.sh - 禁止在测试机上手动修改文件
常见排查和修复
app.py 入口初始化顺序
ServerEnv 必须在 load_rbac / load_appbase 之前设置,否则 rbac 内部调用 env.get_module_dbname('rbac') 时 env 还是空的,抛 TypeError: 'NoneType' object is not callable。
正确顺序:
def init():
env = ServerEnv()
env.get_module_dbname = get_module_dbname # ← 最先
env.password_encode = password_encode
load_appbase()
load_rbac()
# ... 其他初始化
venv 缺少 wheel → pip install -e 失败
错误 invalid command 'bdist_wheel' → venv 未安装 wheel。修复:pip install wheel 再重试。
bricks 软链接路径
bricks 软链接从 wwwroot 目录创建,必须用相对路径:
ln -sf ../pkgs/bricks/dist wwwroot/bricks # ✅ 正确
ln -sf pkgs/bricks/dist wwwroot/bricks # ❌ 从 wwwroot 看找不到
⚠️ wwwroot/bricks 必须是软链接 → pkgs/bricks/dist,绝不能是实体目录(build.sh 标准:rm -rf wwwroot/bricks && ln -sf pkgs/bricks/dist wwwroot/bricks)。手动"修复"时若创建实体目录并复制/链接了 pkgs/bricks/bricks/ 的源文件 bricks.js(~23KB),前端立即崩溃:bricks.js:684 Class extends value undefined + bricks.App is not a constructor → 页面空白。诊断:ls -la wwwroot/ | grep bricks(有无 ->)+ wc -c wwwroot/bricks/bricks.js(编译版 ~424KB,源版 ~23KB)。修好后用户仍报同样错误 → 对比服务器报错行号内容与浏览器报错是否吻合,不吻合就是浏览器缓存,让用户 Ctrl+Shift+R(2026-08 pipeline-app 事故:反复改服务器无果,根因是实体目录+源版 bricks.js)。
非 pip 包的模块:用 PYTHONPATH 不用 pip install -e
如果模块没有 setup.py / pyproject.toml(如 rag-pipeline 是纯 ahserver 应用),不能 pip install。改为在 start.sh 的 PYTHONPATH 中加入该模块路径:
export PYTHONPATH=$cdir:$cdir/pkgs/rag-pipeline:$PYTHONPATH
PYTHONPATH 必须包含应用根目录
入口 app/ragserver.py 中有 from init import xxx,需要应用根目录在 PYTHONPATH 中,否则 ModuleNotFoundError: No module named 'init'。
陈旧 pip 进程清理
多次后台 build 尝试可能残留旧 pip 进程,占用资源并导致后续安装冲突:
pkill -9 -f 'pip install' 2>/dev/null
BricksUIProcessor 需要 .tmpl 处理器
ahserver 的 BricksUIProcessor(处理 .ui 文件)内部会加载 /bricks/header.tmpl 等模板文件。如果 config.json 的 website.processors 缺少 [".tmpl", "tmpl"],会报 AttributeError: 'NoneType' object has no attribute 'be_call'(/ 和 /index.ui 返回 500)。
必须包含:
"processors": [
[".tmpl", "tmpl"],
[".ui", "bui"],
[".dspy", "dspy"],
...
]
sqlor 数据库 driver
- MySQL 必须用
"driver": "mysql",不能用"mysql+aiomysql"。sqlor 的MySqlor.isMe()只匹配mysql/aiosqlor/tidb,其他值导致Not Implemented - kwargs 中禁止
minsize/maxsize— aiomysql.connect 不接受这些参数
dspy 文件返回格式
- ahserver 将 dspy 代码包装为
async def myfunc(request, **ns):后await执行 - 必须用
return语句显式返回值 — 最后一行裸result在 async def 中返回 None async with db.sqlorContext()可在 dspy 中正常工作(因为外层 wraper 是await的)- fallback 模式:
async with内 return 成功值,外 return 错误值
async with db.sqlorContext(dbname) as sor:
...
return {"status": "SUCCEEDED"}
return {"error": "failed"}
app.py 必须 import bricks_for_python 并调用 load_pybricks
ahserver 的 bui 处理器由 bricks_for_python 包注册。入口文件需:
import bricks_for_python
from bricks_for_python.init import load_pybricks
def init():
...
load_pybricks() # 注册 UiWindow, UiError 等工具函数
build.sh 同步 password_key 时仅改单个字段
python3 -c "c=json.load(...); c['password_key']=k; json.dump(c,...)" 是安全的——只改 key 不覆盖 processors/databases。切勿用 sed 替换整行,会破坏 JSON 结构。
SSH key 部署到新测试机
# 生成或复制 key
ssh-keyscan -H git.opencomputing.cn >> ~/.ssh/known_hosts
scp ~/.ssh/id_rsa user@test-host:~/.ssh/id_rsa
git pull 因 config.json 本地修改冲突
每次部署时手动修改 config.json(driver/password 等),导致下次 git pull 冲突。标准流程:
git checkout conf/config.json && git pull
# 然后 python3 脚本重新修正 driver/password
常见检查项
appcodes_kv表需utf8mb4字符集- CRUD 下拉
valueField="value"textField="text"匹配 dspy 返回 - 新 dspy 必须注册 RBAC 权限
created_by自动赋userorgid(xls2ui 会覆盖,build.sh 中 sed 修复)- bricks 链接:
ln -sf pkgs/bricks/dist wwwroot/bricks - bricks build 前置:
mkdir -p ../dist/i18n - menu 点击嵌套全页: wwwroot 子目录缺失→fallback 根 index.ui,需
ln -sf pkgs/*/wwwroot wwwroot/ - pipeline_sdlc 下划线:
ln -sf pipeline-sdlc wwwroot/pipeline_sdlc - config.json 只同步 password_key,不覆盖 databases(应用有独立 DB);DB 密码必须 AES 加密
get_module_dbname(m): return 'pipeline' 全模块统一password_encode(s): if s is None: return '' 兼容 login form 首次加载- RBAC 通配符:
/**非/* - 部署后验证: 逐菜单点击+curl HTTP 码双重确认
- 详细 CRUD 坑汇总: 见
references/bricks-crud-pitfalls.md - pipeline-app 实战经验: 见
references/pipeline-app-pitfalls.md - RBAC 数据库迁移: 见
references/rbac-migration.md - sqlor 配置 (driver/密码/RBAC列名): 见
references/sqlor-config-pitfalls.md - 用户密码必须 RC4 加密 (与 DB 的 AES 不同): 见
references/rbac-user-auth.md - Bricks UI 布局/按钮/菜单约定: 见
references/bricks-ui-conventions.md - RAG 应用部署记录: 见
references/rag-deployment.md - RAG Server 验证工作流 (RBAC session认证 + smoke test): 见
references/ragserver-verification.md - GPU 模型下载: 见
references/gpu-model-download.md