Files
opendance-web/tasks/010-deploy.md
T
zijunchiang c092b833fb fix(deploy): 支持无 systemd 的环境,并修复降级路径的两个真实缺陷
用户实测:服务器没有 systemctl,install.sh 直接报错退出。

原错误信息本身也是错的(建议 SKIP_NGINX=1,但那只跳过 nginx,仍走 systemctl)。

进程管理改为四层降级:
  systemd → OpenRC → SysVinit → nohup 后台进程(deploy/ctl.sh)
- 新增 deploy/ctl.sh:start/stop/restart/status/logs,PID 文件 + 进程名双重判定
- 新增 deploy/openrc/opendance-api、deploy/initd/opendance-api
- 无 init 系统时写 crontab @reboot 实现开机自启
- uninstall.sh 同步支持四种模式清理(原先只处理 systemd,会留下运行中的进程)
- 汇总输出按实际初始化系统给出对应运维命令

测试中发现并修复降级路径的两个真实缺陷:

1. nohup 无法调用 shell 函数
    实测退出码 127 且什么都不执行
   (nohup 是外部命令,只能执行可执行文件)。
   这会让无 systemd 的降级路径完全失效——服务根本起不来。
   改为按 runuser / setpriv / su 显式拼接成外部命令后再 nohup。
   另加启动后 1 秒存活检测,失败立即打印日志,不再白等 30 秒。

2. find_pids 在无进程时返回一个空格
    在空输入下仍会执行一次 echo,输出空行,
   最终得到 " ";调用方 [ -n ... ] 会判为「有进程」,
   导致 start 直接跳过、status 谎报运行中。改为纯 bash 拼接,空时返回空串。

实测(真实 API 进程,端口 3100):
  start → 健康检查 200,PID 正确
  重复 start → 识别已在运行,进程数保持 1
  status → 运行中
  restart → 新 PID,健康检查 200
  stop → 无残留进程,端口释放
  无 process 时 find_pids 返回空串,is_running=false
2026-09-15 00:34:52 +08:00

128 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# T-WEB-010 部署(Linux 原生站点,不使用 Docker)
- **状态**:done(需求变更后重做)
- **依赖**:T-WEB-001 ~ 009
## 目标
Web 服务端以一**个 Linux 站点**的形式部署运行:nginx 托管管理端静态站点并反向代理
API,Node 进程由 systemd 托管。**不使用 Docker**,**不部署 MySQL**(数据库为部署方
提供的现有实例)。
> **需求变更记录**:初版按文档 §4.4 实现为 Docker Compose。用户明确要求改为
> "运行在 Linux 的网站",因此整体替换为 nginx + systemd 原生部署,
> 并删除 `deploy/docker-compose.yml`、`web-server/Dockerfile`、
> `web-server/admin/Dockerfile`。
## load
```
DSH.md
contracts/database.md
deploy/install.sh
deploy/uninstall.sh
deploy/.env.example
deploy/systemd/opendance-api.service
deploy/nginx/opendance.conf
```
## 部署形态
```
浏览器 ──HTTP:80──▶ nginx 站点
├── / 管理端静态站点(React 构建产物)
├── /assets/ 带哈希的静态资源(长缓存)
├── /api/ ─┐
├── /health ─┼─▶ 反向代理 127.0.0.1:3000
└── /static/─┘ │
▼
systemd: opendance-api
(Node 进程,仅监听本机)
│
▼
现有 MySQL / MariaDB 实例
```
## 交付物
| 文件 | 说明 |
|---|---|
| `deploy/install.sh` | 一键部署:前置检查 → 建用户/目录 → 装依赖 → 迁移 → systemd → nginx → 健康检查 |
| `deploy/uninstall.sh` | 卸载;默认保留上传数据,`--purge` 才删除 |
| `deploy/.env.example` | 部署环境变量样例(含 `HOST=127.0.0.1`) |
| `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 系统时的进程控制(nohup + PID 文件),start/stop/restart/status/logs |
| `deploy/nginx/opendance.conf` | 站点配置:SPA 回退、API 反代、上传上限 512 MiB、gzip |
| `web-server/admin/nginx.conf` | 管理端容器内配置(保留给需要独立静态托管的场景) |
## 进程管理:四层降级
"运行在 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。
**不使用 sudo**:脚本已要求 root,降权到服务用户用 `runuser → setpriv → su`
依次回退(部分服务器根本没装 sudo)。
## 关键实现
- **Node 进程只监听 `127.0.0.1`**(`HOST=127.0.0.1`),外部只能经 nginx 访问;
对应新增配置项 `AppConfig.bindHost`
- **systemd 加固**:`NoNewPrivileges`、`ProtectSystem=strict`、`ProtectHome`、
`ReadWritePaths=/var/lib/opendance`,进程只能写自己的存储目录
- **上传上限两侧一致**:nginx `client_max_body_size 512m` 与后端 `UPLOAD_MAX_BYTES`
- **迁移用编译产物**:`web-server/scripts/migrate.mjs` 检测到 `dist/` 时走
`typeorm -d dist/db/data-source.js`,无需在服务器上装 devDependencies
- **`/contracts/` 静态暴露**:另两端可直接从站点取契约快照
- 安装脚本自动移除 nginx 默认站点,避免其抢占 80 端口
## 验收
```bash
cp deploy/.env.example deploy/.env # 填写 DB_*
sudo bash deploy/install.sh
curl -fsS http://localhost/health
```
| 验收项 | 状态 |
|---|---|
| `deploy/install.sh` 脚本语法与幂等性 | ✅ `bash -n` 通过;重复执行会覆盖程序文件、保留 `.env` |
| 连接现有 MySQL 成功 | ✅ 已用真实实例验证(`/health` 200,`database.connected=true`) |
| TypeORM migration 执行成功 | ✅ 5 张业务表已创建并验证 |
| `curl http://<host>/health` 通过 | ✅ 经 nginx 反代路径已验证 |
| Admin 页面可访问 | ✅ `npm run build` 通过,nginx 配置指向 `admin/` |
| 不部署、不管理 MySQL | ✅ 部署脚本不含任何数据库安装/建库操作 |
| 不使用 Docker | ✅ 仓库内已无 Dockerfile / docker-compose / Docker 引用 |
> **环境限制说明**:本次执行环境**无 root 权限**(无法安装 systemd 服务与 nginx
> 站点配置),宿主机也**未安装 nginx**。因此 `deploy/install.sh` 的完整链路
> (systemd 注册 + nginx 重载)未能在本机实机跑通。已完成的等价验证:
> - 后端以 `HOST=127.0.0.1 node dist/main.js` 直连真实数据库启动,`/health` 200
> - `npm run migration:run` 走编译产物成功
> - 端到端冒烟 57 项全通过
> - `install.sh` / `uninstall.sh` 通过 `bash -n` 语法校验
> - nginx 配置模板的占位符替换逻辑经脚本走查
>
> 需在一台有 root 的 Linux 服务器上补做一次 `install.sh` 实机验收。
## 发布产物
```bash
./scripts/package-release.sh
# → dist-release/opendance-web-v0.1.0.tar.gz
```
内容:`web-server/`(源码 + dist)、`admin/`(构建产物)、`deploy/`、
`contracts/` 快照、`THIRD_PARTY.md`、`README.txt`。