266 lines
17 KiB
Markdown
266 lines
17 KiB
Markdown
# T-WEB-010 部署(Linux 业务服务,不使用 Docker,不部署反代)
|
||
|
||
- **状态**:done(两次需求变更后重做)
|
||
- **依赖**:T-WEB-001 ~ 009
|
||
|
||
## 目标
|
||
|
||
Web 服务端以**一个 Linux 业务服务**的形式部署运行:Node 进程由系统服务
|
||
(systemd / OpenRC / SysVinit / 后台进程)托管,**同时提供 REST API 与管理端页面**。
|
||
**不使用 Docker**,**不部署 MySQL**(数据库为部署方提供的现有实例),
|
||
**也不部署反向代理**(由另外的服务/团队负责)。
|
||
|
||
> **需求变更记录**
|
||
> 1. 初版按文档 4.4 实现为 Docker Compose;用户要求改为"运行在 Linux 的网站",
|
||
> 于是替换为 nginx + systemd 原生站点部署,并删除 `deploy/docker-compose.yml`、
|
||
> `web-server/Dockerfile`、`web-server/admin/Dockerfile`。
|
||
> 2. 随后用户明确:"web 端仅需要业务需求,反代等工作交由另外的服务处理。
|
||
> 只需要给出 iis 和 nginx 反代的说明文档,不用部署反代服务。"
|
||
> 于是 install.sh **彻底移除 nginx 的安装与站点配置**,管理端改由 Node 服务自身
|
||
> 托管(`ADMIN_DIR` + SPA 深链接回退),并新增 `deploy/proxy/` 只交付说明文档。
|
||
> 3. 再随后用户要求**管理端口与软件 API 端口分离**:新增 `ADMIN_PORT`
|
||
> (默认 `PORT + 1`)与可选的 `ADMIN_HOST`(默认跟随 `HOST`)。软件 API 端口
|
||
> 只给客户端调接口(`/api`、`/health`、`/static`、`/contracts`,`/` 等返回 404),
|
||
> 管理端端口给浏览器(管理页面 + 全部接口)。`ADMIN_PORT === PORT` 保留为
|
||
> 单端口兼容模式,只 warn 不中止。install.sh 的汇总与反代说明改为两个端口。
|
||
> 4. 本次追加两项:
|
||
> a) **首次启动的.env 引导**:新增 `deploy/init-env.sh`(交互式逐项询问数据库
|
||
> 与端口,写盘前校验必填项/端口范围,可用 Node + mysql2 做连通性自检);
|
||
> `install.sh` 在缺 `deploy/.env` 时先调用它,失败才 die(安装目录已有上次
|
||
> 安装留下的.env 则直接复用,保持原语义)。
|
||
> b) **默认监听 0.0.0.0 固化**:确认 `configuration.ts` 的
|
||
> `bindHost = process.env.HOST?? '0.0.0.0'`、`adminHost` 默认跟随 HOST,
|
||
> 两个 `.env.example` 与 `init-env.sh` 的 HOST 默认值均为 `0.0.0.0`。
|
||
|
||
## load
|
||
|
||
```
|
||
DSH.md
|
||
contracts/database.md
|
||
deploy/init-env.sh
|
||
deploy/install.sh
|
||
deploy/uninstall.sh
|
||
deploy/.env.example
|
||
deploy/systemd/opendance-api.service
|
||
deploy/proxy/README.md
|
||
```
|
||
|
||
## 部署形态
|
||
|
||
**职责边界**:本端只提供**业务服务**;反向代理由另外的服务/团队负责。
|
||
|
||
```
|
||
Windows/Godot 客户端、Android App ─▶ 软件 API 端口:3000(只提供接口,无管理页面)
|
||
├── /api/ REST API
|
||
├── /health 健康检查(数据库不可用返回 503)
|
||
├── /static/ 上传文件
|
||
└── /contracts/ 契约快照
|
||
(/、/songs 等返回 404,有意设计)
|
||
|
||
浏览器 ─▶ 反向代理(外部提供,IIS / nginx / 网关...)──▶ 管理端端口:3001
|
||
│ 整端口转发,不需要托管静态文件 ├── / 管理端页面(含 SPA 深链接回退)
|
||
│ ├── /assets/ 前端静态资源
|
||
│ └── 上面全部接口
|
||
▼
|
||
现有 MySQL / MariaDB 实例(两个端口共用同一 Node 进程)
|
||
```
|
||
|
||
本服务一个 Node 进程**同时监听两个端口**(软件 API 端口 + 管理端端口),
|
||
管理页面只在管理端口提供;反代按用途分别转发,每个端口各一条全匹配规则即可,
|
||
既不用在代理层托管静态文件,也不用配置 SPA 回退(`try_files`)。
|
||
|
||
`deploy/install.sh` **不安装、不配置任何反向代理软件**,收尾时只打印反代交接指引。
|
||
|
||
## 交付物
|
||
|
||
| 文件 | 说明 |
|
||
|--|--|
|
||
| `deploy/install.sh` | 一键部署:前置检查 -> 建用户/目录 -> 复制并校验文件 -> 装依赖 -> 迁移 -> 注册系统服务 -> 健康检查;汇总打印两个端口与防火墙放行提示;缺 `deploy/.env` 时先调用 `init-env.sh` 引导 |
|
||
| `deploy/init-env.sh` | **首次启动的 `.env` 交互引导**:逐项询问 8 个键、写盘前校验必填项与端口范围、可选数据库连通性自检;只替换/追加这 8 个键并保留模板注释,写完 `chmod 600` 并回读;`--check` / `-y` 等非交互模式下绝不阻塞 |
|
||
| `deploy/uninstall.sh` | 卸载;默认保留上传数据,`--purge` 才删除 |
|
||
| `deploy/.env.example` | 部署环境变量样例(`PORT` / `ADMIN_PORT` / `ADMIN_HOST` / `HOST` 说明与上传上限告警) |
|
||
| `deploy/systemd/opendance-api.service` | systemd 单元,含 `ProtectSystem=strict` 等安全加固 |
|
||
| `deploy/openrc/opendance-api` | OpenRC 服务脚本 |
|
||
| `deploy/initd/opendance-api` | SysVinit / LSB 启动脚本(转调 ctl.sh) |
|
||
| `deploy/ctl.sh` | 无 init 系统时的进程控制(后台进程 + PID 文件),start/stop/restart/status/logs |
|
||
| `deploy/proxy/README.md` | **反代说明文档**:转发目标、绑定地址、自检清单、故障对照 |
|
||
| `deploy/proxy/nginx.conf.example` | nginx 配置样例(上传上限、超时、keepalive、可选 HTTPS) |
|
||
| `deploy/proxy/web.config.example` | IIS 配置样例(需 ARR + URL Rewrite 模块) |
|
||
|
||
## deploy/.env 初始化引导(init-env.sh)
|
||
|
||
首次部署不再要求手工 `cp deploy/.env.example deploy/.env && vi deploy/.env`:
|
||
|
||
```
|
||
install.sh 检测到缺 deploy/.env
|
||
├── $APP_DIR/deploy/.env 存在 -> 直接复用(上次安装留下的配置,原语义保留)
|
||
└── 否则调用 deploy/init-env.sh
|
||
├── 缺.env -> 从.env.example 复制,chmod 600
|
||
├── 交互(TTY / 管道)-> 逐项询问并写回
|
||
└── 非交互(CI、</dev/null、-y)-> 只校验,绝不阻塞;失败则 install.sh die
|
||
```
|
||
|
||
逐项询问 `DB_HOST / DB_PORT / DB_USER / DB_PASS / DB_NAME / PORT / ADMIN_PORT / HOST`
|
||
共 8 个键,回车保留默认;`DB_PASS` 用 `read -rs` 不回显、按约定写盘**不加引号**;
|
||
校验必填项与端口范围;可用 Node + mysql2 做连通性自检(口令只经环境变量传给子进程,
|
||
失败仅告警);只替换/追加这 8 个键、保留模板注释,写完 `chmod 600` 并回读校验;
|
||
结尾打印摘要(口令 `***`)与本次变更列表。
|
||
|
||
| 开关 | 说明 |
|
||
|--|--|
|
||
| (无) | 交互引导;`.env` 已存在且必填项齐全时跳过询问 |
|
||
| `--force` | 已有 `.env` 也重新逐项询问 |
|
||
| `--check` | 只校验,不修改文件、不提问 |
|
||
| `-y` / `--yes` / `--non-interactive` | 非交互:只校验缺字段与端口,绝不提问 |
|
||
| `-i` / `--interactive` | 即使 stdin 不是 TTY 也强制提问(读取超时仍生效) |
|
||
| `--skip-db-check` | 跳过数据库连通性自检 |
|
||
| `--dir DIR` | 等价于 `--env-file DIR/.env --example DIR/.env.example` |
|
||
| `--env-file PATH` / `--example PATH` | 指定 `.env` / 模板路径 |
|
||
| `--timeout SECONDS` | 非 TTY 下每次读取超时秒数(默认 10) |
|
||
| `-h` / `--help` | 显示帮助 |
|
||
|
||
环境变量:`OPENDANCE_NONINTERACTIVE` / `OPENDANCE_INTERACTIVE` /
|
||
`OPENDANCE_SKIP_DB_CHECK` / `OPENDANCE_ENV_FILE` / `OPENDANCE_ENV_EXAMPLE` /
|
||
`OPENDANCE_PROMPT_TIMEOUT`。
|
||
|
||
**非交互绝不挂住**:stdin 非 TTY 且是 `</dev/null` 或已关闭,或带 `-y` 等开关 ->
|
||
只校验不提问;所有 read 带超时,EOF/超时立即 `exit 1`。
|
||
|
||
**默认监听 0.0.0.0**:`configuration.ts` 的
|
||
`bindHost = process.env.HOST?? '0.0.0.0'`,`adminHost` 默认跟随 `HOST`
|
||
(HOST 未设置时同样回落 `0.0.0.0`);`deploy/.env.example` 与
|
||
`web-server/.env.example` 均写 `HOST=0.0.0.0`;`install.sh` 的
|
||
`BIND_HOST="${HOST:-0.0.0.0}"`;`init-env.sh` 的 HOST 默认值也是 `0.0.0.0`。
|
||
注意 `install.sh` 里的 `HEALTH_HOST` / `API_HOST` 用的是**回环地址**做本机健康检查,
|
||
与对外监听的默认值无关。
|
||
|
||
## 进程管理:四层降级
|
||
|
||
"运行在 Linux 上"不应等于"必须有 systemd"。安装时按以下顺序探测并选择:
|
||
|
||
```
|
||
1. systemd /run/systemd/system 存在且 systemctl 可用
|
||
2. OpenRC rc-service + rc-update 可用
|
||
3. SysVinit /etc/init.d 存在且 update-rc.d 或 chkconfig 可用
|
||
4. 都没有 nohup 后台进程 + PID 文件(deploy/ctl.sh)
|
||
并写 crontab @reboot 实现开机自启
|
||
```
|
||
|
||
三种 init 方式最终都调用同一个 Node 进程,只是守护方式不同;第 4 种由
|
||
`deploy/ctl.sh` 提供 start/stop/restart/status/logs。
|
||
(注意:`nohup` 不能调用 shell 函数,故 `ctl.sh` 用
|
||
`spawn_service_process()` 拼装外部命令,而非包装函数。)
|
||
|
||
**不使用 sudo**:脚本已要求 root,降权到服务用户用 `runuser -> setpriv -> su`
|
||
依次回退(部分服务器根本没装 sudo)。
|
||
|
||
## 关键实现
|
||
|
||
- **双端口**:一个 Node 进程创建两个 HTTP server--软件 API 端口(`PORT`,默认 3000)
|
||
与管理端端口(`ADMIN_PORT`,默认 3001),复用同一个 express 实例;用
|
||
`req.socket.localPort` 判断请求来自哪个端口,决定是否提供管理页面。
|
||
`ADMIN_PORT === PORT` 时只建一个 server(单端口兼容模式,避免 EADDRINUSE)
|
||
- **服务自身托管管理端**:`app.useStaticAssets(adminDir,...)` + SPA 回退中间件
|
||
(仅管理端口生效);回退会跳过 `/api/`、`/health`、`/static/`、`/contracts/`
|
||
以及带扩展名的路径,保证未知 API 仍返回 404 而不是 HTML
|
||
- **软件 API 端口不提供管理页面**:`/`、`/songs` 等在该端口一律 404(有意设计)
|
||
- **`HOST` / `ADMIN_HOST` 绑定地址**:同机反代建议 `127.0.0.1`,异机反代用
|
||
`0.0.0.0` + 防火墙;`ADMIN_HOST` 可选(默认跟随 `HOST`),可单独把管理端限制在
|
||
本机或内网;`HOST` 默认取 `0.0.0.0` 以保证开箱可用
|
||
- **首次部署的.env 由引导生成**:`install.sh` 缺 `deploy/.env` 时先调用
|
||
`deploy/init-env.sh`(安装目录已有上次安装的 `.env` 则直接复用);引导脚本在
|
||
非交互环境下只校验、绝不阻塞,校验失败时 install.sh 才 die
|
||
- **端口校验**:`PORT` / `ADMIN_PORT` 必须是 1..65535 的整数;非法值在生产环境
|
||
启动时快速失败(`validateEnv` 的 errors),`ADMIN_PORT === PORT` 只 warn
|
||
- **install.sh 汇总与反代说明**:结尾分别打印两个端口的访问地址、监听配置与
|
||
防火墙放行命令(`firewall-cmd` / `ufw` 同时放行两个端口)
|
||
- **systemd 加固**:`NoNewPrivileges`、`ProtectSystem=strict`、`ProtectHome`、
|
||
`ReadWritePaths=/var/lib/opendance`,进程只能写自己的存储目录
|
||
- **上传上限两侧一致**:后端 `UPLOAD_MAX_BYTES=536870912`(512 MiB),
|
||
反代侧 `client_max_body_size` / `maxAllowedContentLength` 必须不小于此值
|
||
- **模型转换配置**(`deploy/.env.example` 已写入):
|
||
- `ASSIMP_BIN=assimp`:上传 `.zip` 模型包或 `.pmx`/`.fbx` 单文件时**必须**能调用到,
|
||
否则转换失败 **422**
|
||
- `MODEL_CONVERT_FORMAT=glb`(默认)|`vrm`:只影响需要转换的 PMX/FBX 的交付格式。
|
||
`glb` 通用,两个客户端都原生加载;`vrm` 在 GLB 之上补 `VRMC_vrm` 扩展
|
||
(humanoid 骨骼 / 表情语义),**humanoid 骨骼名映射不全时自动回退交付 GLB**
|
||
并写进 `models.conversion_note`。产物一律 `assimp info` 回读校验
|
||
- `MODEL_CONVERT_PMX` / `MODEL_CONVERT_TIMEOUT_MS` 语义见 README 四
|
||
- **迁移用编译产物**:`web-server/scripts/migrate.mjs` 检测到 `dist/` 时走
|
||
`typeorm -d dist/db/data-source.js`,无需在服务器上装 devDependencies;
|
||
本版本迁移共 3 个:`InitSchema1789382903060`、
|
||
`AddModelConversionFields1789450000000`、**`AddModelStats1789460000000`**
|
||
(新增 `models.stats TEXT`,存模型分析结果,已在真实库执行)
|
||
- **依赖安装显式带 dev**:安装脚本先 `set -a;. deploy/.env` 导出了
|
||
`NODE_ENV=production`,会让 npm 跳过 devDependencies 导致 `tsc: not found`;
|
||
故用 `env -u NODE_ENV npm ci --include=dev`
|
||
- **复制文件时锚定排除**:`storage/` 这类模式会误伤 `src/storage/` 与 `dist/storage/`
|
||
(曾导致 `MODULE_NOT_FOUND...storage.module`),排除规则一律用 `/storage/` 锚定
|
||
- **`/contracts/` 静态暴露**:另两端可直接从站点取契约快照
|
||
|
||
## 验收
|
||
|
||
```bash
|
||
bash deploy/init-env.sh # 或手工 cp deploy/.env.example deploy/.env 后填写 DB_*
|
||
bash deploy/install.sh # 需 root(不需要 sudo)
|
||
curl -fsS http://localhost:3000/health
|
||
curl -fsS http://localhost:3001/health
|
||
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/ # 期望 404(软件 API 端口无管理页面)
|
||
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3001/ # 期望 200(管理页面)
|
||
```
|
||
|
||
| 验收项 | 状态 |
|
||
|--|--|
|
||
| `deploy/install.sh` 脚本语法与幂等性 | ✅ `bash -n` 通过;重复执行会覆盖程序文件、保留 `.env` |
|
||
| 连接现有 MySQL 成功 | ✅ 已用真实实例验证(`/health` 200,`database.connected=true`) |
|
||
| TypeORM migration 执行成功 | ✅ 5 张业务表已创建并验证;v0.2.0 的 `AddModelStats1789460000000`(`models.stats TEXT`)已在真实库执行 |
|
||
| 模型转换配置 | ✅ `deploy/.env.example` 写入 `ASSIMP_BIN` / `MODEL_CONVERT_FORMAT=glb|vrm` / `MODEL_CONVERT_PMX` / `MODEL_CONVERT_TIMEOUT_MS`;不合规输入转换失败 422 |
|
||
| `curl http://<host>:3000/health` 通过 | ✅ 直连后端端口已验证 |
|
||
| 软件 API 端口 `/` 返回 404 | ✅ `PORT=3000` 上 `/`、`/songs` 均 404(有意设计,无管理页面) |
|
||
| 管理端端口 `/` 返回 200 | ✅ `ADMIN_PORT=3001` 上 `/`、`/songs`、`/models` 均返回同一份 HTML(200) |
|
||
| 前端深链接回退 | ✅ 未知静态路径回退到 `index.html`,未知 API 仍 404 |
|
||
| `ADMIN_PORT === PORT` 单端口兼容模式 | ✅ 只建一个 server,管理页面回到 API 端口,日志提示「未分端口」 |
|
||
| 端口非法在生产环境中止启动 | ✅ `validateEnv` 对非 1..65535 整数端口报 error |
|
||
| 不部署、不管理 MySQL | ✅ 部署脚本不含任何数据库安装/建库操作 |
|
||
| 不使用 Docker | ✅ 仓库内已无 Dockerfile / docker-compose / Docker 引用 |
|
||
| 不部署反向代理 | ✅ 仅交付 `deploy/proxy/` 文档,install.sh 中无任何反代安装动作 |
|
||
| 缺 `deploy/.env` 时由 `init-env.sh` 引导 | ✅ 交互逐项询问 8 个键;只改这 8 个键、保留模板注释、`chmod 600`、回读校验;安装目录已有 `.env` 时直接复用 |
|
||
| `init-env.sh --check` / 非交互不阻塞 | ✅ `test/init-env.spec.ts`(16 项)覆盖 `--check`、`-y`、EOF/超时立即退出、口令不加引号写盘 |
|
||
| `HOST` 默认 `0.0.0.0` | ✅ `configuration.ts`、`deploy/.env.example`、`web-server/.env.example`、`install.sh`、`init-env.sh` 五处一致 |
|
||
|
||
> **环境限制说明**:本次执行环境**无 root 权限**,且**未安装 nginx**(按需求也不应安装)。
|
||
> 因此 `deploy/install.sh` 的服务注册链路(systemd / OpenRC / SysVinit / crontab)
|
||
> 未能在本机实机跑通。已完成的等价验证:
|
||
> - 后端以 `node dist/main.js` 直连真实数据库启动,两个端口 `/health` 均 200
|
||
> - 管理端托管与 SPA 回退逐路径实测(`/`、`/songs`、`/models`、`/api/*`、未知路径),
|
||
> 并确认 3000 上返回 404、3001 上返回 200
|
||
> - `ADMIN_DIR` 环境变量生效,`index.html` 缺失时打印告警而非崩溃
|
||
> - `npm run migration:run` 走编译产物成功
|
||
> - 端到端冒烟 57 项全通过
|
||
> - `install.sh` / `uninstall.sh` / `ctl.sh` 通过 `bash -n` 语法校验
|
||
> - `init-env.sh` 通过 `bash -n`,并由 `test/init-env.spec.ts`(16 项)覆盖
|
||
> 交互写回 / `--check` / 非交互 / 超时 / 幂等 / 口令不加引号
|
||
> - 反代文档中的占位符与后端实际行为逐条对照(两个端口、路径、上传上限、健康检查)
|
||
>
|
||
> 需在一台有 root 的 Linux 服务器上补做一次 `install.sh` 实机验收。
|
||
|
||
## 发布产物
|
||
|
||
```bash
|
||
./scripts/package-release.sh
|
||
# -> dist-release/opendance-web-v0.2.0.tar.gz
|
||
```
|
||
|
||
内容:`web-server/`(源码 + dist)、`admin/`(构建产物)、`deploy/`
|
||
(含 `proxy/` 反代文档)、`contracts/` 快照、`THIRD_PARTY.md`、`README.txt`。
|
||
打包脚本会对产物做硬校验:`src/**/*.ts` 必须都有对应 `dist/*.js`,
|
||
关键文件(`dist/main.js`、`dist/storage/storage.module.js`、`admin/index.html`、
|
||
`deploy/proxy/*`)必须齐全,且不得含真实 `.env` 与 Docker/nginx 安装配置。
|
||
v0.2.0 起校验清单追加模型新模块:`dist/models/model-inspector.js`、
|
||
`dist/models/model-thumbnail.js`、`dist/models/vrm-writer.js`、
|
||
`dist/models/gltf-packer.js`、`dist/migrations/1789460000000-AddModelStats.js`。
|
||
|
||
> **发布包名变更**:`opendance-web-v0.1.0.tar.gz` -> **`opendance-web-v0.2.0.tar.gz`**
|
||
> (`scripts/package-release.sh` 的 `VERSION` 默认值已改为 `0.2.0`,
|
||
> 与 `project.yaml` 的 `version: 0.2.0`、`web-server/package.json` 一致)。
|