224 lines
7.6 KiB
Markdown
224 lines
7.6 KiB
Markdown
# 部署与运维
|
||
|
||
## 配置
|
||
|
||
Docker Compose、直接运行的 Node 服务和管理员 CLI 都会从项目根目录
|
||
`.env` 读取配置。已由 shell、PM2 或系统服务注入的同名变量优先。设置
|
||
`ENV_FILE=/path/to/file` 可改用其他配置文件;显式指定的文件不存在时,
|
||
进程会拒绝启动。
|
||
|
||
| 变量 | 说明 | 默认值 |
|
||
| --- | --- | --- |
|
||
| `NODE_ENV` | 运行环境:`development`、`test` 或 `production` | `development` |
|
||
| `HOST` | Node 服务监听地址 | `127.0.0.1` |
|
||
| `PORT` | Node 服务监听端口 | `3000` |
|
||
| `PUBLIC_ORIGIN` | production 对外 HTTPS origin | production 必填 |
|
||
| `DATA_DIRECTORY` | SQLite 与附件目录 | `./data` |
|
||
| `FEEDBACK_DOMAIN` | Caddy 申请证书并对外服务的域名 | 必填 |
|
||
| `APP_SECRET` | 会话、IP 和安装标识 digest 的主 secret,不限制长度 | 自动生成并持久化 |
|
||
| `INITIAL_ADMIN_EMAIL` | 数据库为空时创建的初始管理员邮箱 | 可选 |
|
||
| `INITIAL_ADMIN_PASSWORD` | 初始管理员密码,12–128 字符 | 可选 |
|
||
| `INITIAL_ADMIN_NAME` | 初始管理员显示名 | `Administrator` |
|
||
| `SESSION_TTL_HOURS` | 管理员会话时长 | `12` |
|
||
| `PUBLIC_RATE_LIMIT_PER_HOUR` | 单 IP 每小时公开提交上限 | `20` |
|
||
| `PUBLIC_INSTALLATION_LIMIT_PER_HOUR` | 单安装实例每小时公开提交上限 | `8` |
|
||
| `MAX_SCREENSHOT_BYTES` | 输入及标准化后截图的字节上限 | `5242880` |
|
||
|
||
`.env` 包含 secret,不得提交到 Git 或复制到工单、聊天和日志中。建议在
|
||
密码管理器中保存一份加密副本,并在主机上执行 `chmod 600 .env`。
|
||
|
||
未设置或留空 `APP_SECRET` 时,应用会生成随机值并以权限 `0600` 保存到
|
||
`DATA_DIRECTORY/.app-secret`。后续启动会复用该文件。应将此文件与
|
||
SQLite 和附件一起备份,否则恢复后现有管理员会话将失效。
|
||
|
||
`INITIAL_ADMIN_EMAIL` 和 `INITIAL_ADMIN_PASSWORD` 必须同时设置。应用仅在
|
||
管理员表为空时创建该账号,后续重启不会同步或重置密码。确认初始账号
|
||
可登录后,应从 `.env` 删除 `INITIAL_ADMIN_PASSWORD`。
|
||
|
||
Compose 会从 `FEEDBACK_DOMAIN` 生成
|
||
`PUBLIC_ORIGIN=https://FEEDBACK_DOMAIN`。直接运行 Node 服务时,
|
||
production 环境仍必须显式提供只含 HTTPS origin 的 `PUBLIC_ORIGIN`。
|
||
|
||
## PM2 部署
|
||
|
||
没有 Docker 时,可通过 Node.js 24+ 和 PM2 启动服务。先安装依赖并准备
|
||
配置:
|
||
|
||
```sh
|
||
npm ci
|
||
npm install --global pm2
|
||
cp .env.example .env
|
||
```
|
||
|
||
复制后的示例配置可以直接以 development 模式启动,初始账号为
|
||
`admin@example.com`,示例密码仅用于首次本地运行。公网部署前必须修改
|
||
初始管理员凭据并配置 production 对外地址:
|
||
|
||
```dotenv
|
||
NODE_ENV=production
|
||
HOST=127.0.0.1
|
||
PORT=3000
|
||
PUBLIC_ORIGIN=https://feedback.example.com
|
||
DATA_DIRECTORY=./data
|
||
APP_SECRET=
|
||
INITIAL_ADMIN_EMAIL=admin@example.com
|
||
INITIAL_ADMIN_PASSWORD=replace-with-a-secure-password
|
||
INITIAL_ADMIN_NAME=Administrator
|
||
```
|
||
|
||
构建并启动:
|
||
|
||
```sh
|
||
npm run pm2:start
|
||
pm2 save
|
||
pm2 status
|
||
curl --fail "http://127.0.0.1:3000/health"
|
||
```
|
||
|
||
登录成功后删除 `.env` 中的 `INITIAL_ADMIN_PASSWORD`,再执行
|
||
`npm run pm2:reload`。数据库已有管理员时,即使保留这些变量也不会重置
|
||
账号,但不应长期把明文密码保存在主机配置中。
|
||
|
||
`ecosystem.config.cjs` 固定使用单进程 fork 模式,避免多个进程同时管理单机
|
||
SQLite 和本地附件。更新代码后执行:
|
||
|
||
```sh
|
||
npm ci
|
||
npm run pm2:reload
|
||
pm2 logs product-issue --lines 100
|
||
```
|
||
|
||
PM2 只管理 Node 进程,不提供 TLS。公网部署仍须在前方配置 Caddy、Nginx
|
||
或等效 HTTPS 反向代理,并且只让代理访问监听端口。
|
||
|
||
## 网络与 TLS
|
||
|
||
- DNS A/AAAA 记录必须指向主机。
|
||
- 入站开放 TCP 80、TCP 443 和 UDP 443。
|
||
- 不要直接发布应用容器的 3000 端口。
|
||
- Caddy 自动申请并续期证书。
|
||
- `TRUST_PROXY=true` 仅用于当前 Caddy → app 的受控容器网络拓扑;改变
|
||
代理层级时必须重新检查真实客户端 IP 解析。
|
||
|
||
验证:
|
||
|
||
```sh
|
||
docker compose ps
|
||
curl --fail "https://feedback.example.com/health"
|
||
curl -I "https://feedback.example.com/admin/"
|
||
```
|
||
|
||
将示例域名替换为 `.env` 中的真实 `FEEDBACK_DOMAIN`。
|
||
|
||
## 管理员
|
||
|
||
创建或重置管理员:
|
||
|
||
```sh
|
||
read -rsp "Administrator password: " PRODUCT_ISSUE_ADMIN_PASSWORD
|
||
echo
|
||
export PRODUCT_ISSUE_ADMIN_PASSWORD
|
||
docker compose run --rm \
|
||
-e PRODUCT_ISSUE_ADMIN_PASSWORD \
|
||
app node dist/server/server/cli/create-admin.js \
|
||
--email admin@example.com \
|
||
--name "MesaLogo Admin"
|
||
unset PRODUCT_ISSUE_ADMIN_PASSWORD
|
||
```
|
||
|
||
密码必须为 12–128 字符。相同邮箱再次执行会更新显示名和密码、重新启用
|
||
管理员,并删除该管理员的所有既有会话。
|
||
|
||
## 备份
|
||
|
||
应用数据全部位于 `feedback-data` volume:
|
||
|
||
- `product-issue.sqlite` 以及可能存在的 `-wal` / `-shm`
|
||
- `attachments/<feedback-id>/screenshot.png`
|
||
|
||
为获得一致快照,备份时短暂停止 app:
|
||
|
||
```sh
|
||
umask 077
|
||
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
||
mkdir -p "backups/${STAMP}"
|
||
docker compose stop app
|
||
docker compose cp app:/app/data/. "backups/${STAMP}/"
|
||
docker compose start app
|
||
tar -C backups -czf "backups/product-issue-${STAMP}.tar.gz" "${STAMP}"
|
||
```
|
||
|
||
同时安全备份 `.env` 中的生产配置,但应与数据归档分开加密保存。不要只
|
||
备份 SQLite 主文件而遗漏 WAL 或 attachments。
|
||
|
||
建议至少每日备份,并定期把加密副本同步到另一台主机或对象存储。定期在
|
||
隔离环境执行恢复演练,而不是只检查归档文件存在。
|
||
|
||
## 恢复
|
||
|
||
恢复会替换当前 volume 内容。先保存当前数据并确认选中的归档:
|
||
|
||
```sh
|
||
mkdir -p restore
|
||
tar -C restore -xzf backups/product-issue-YYYYMMDDTHHMMSSZ.tar.gz
|
||
|
||
docker compose stop app
|
||
docker compose run --rm --no-deps app sh -c \
|
||
'find /app/data -mindepth 1 -maxdepth 1 -exec rm -rf {} +'
|
||
docker compose run --rm --no-deps \
|
||
-v "$PWD/restore/YYYYMMDDTHHMMSSZ:/restore:ro" \
|
||
app sh -c 'cp -R /restore/. /app/data/'
|
||
docker compose start app
|
||
```
|
||
|
||
随后检查:
|
||
|
||
```sh
|
||
docker compose logs --tail=100 app
|
||
curl --fail "https://feedback.example.com/health"
|
||
```
|
||
|
||
登录后台并抽查产品、反馈、备注和截图。
|
||
|
||
## 升级
|
||
|
||
1. 创建并验证当前备份。
|
||
2. 获取经过审核的新版本源码。
|
||
3. 运行 `docker compose build --pull`。
|
||
4. 运行 `docker compose up -d`。
|
||
5. 检查 health、容器日志、管理员登录和一条测试反馈。
|
||
|
||
数据库迁移在应用启动时执行,采用可重复的 `CREATE TABLE/INDEX IF NOT
|
||
EXISTS`。未来出现结构变更时,仍应先备份再升级。
|
||
|
||
## Secret 轮换
|
||
|
||
更新 `.env` 中的 `APP_SECRET`,然后重建 app:
|
||
|
||
```sh
|
||
docker compose up -d --force-recreate app
|
||
```
|
||
|
||
轮换会让所有管理员会话失效,并使后续 IP/安装标识使用新的 digest。
|
||
数据库中的反馈、备注和截图不依赖该 secret 解密,因此仍可读取。
|
||
|
||
## 监控与故障排查
|
||
|
||
```sh
|
||
docker compose ps
|
||
docker compose logs --tail=200 app
|
||
docker compose logs --tail=200 caddy
|
||
docker compose exec app node -e \
|
||
"fetch('http://127.0.0.1:3000/health').then(async r=>console.log(r.status,await r.text()))"
|
||
```
|
||
|
||
应用关闭了常规逐请求访问日志,避免把公开提交者的原始 IP 持久写入应用
|
||
日志。拒绝和内部错误使用请求 ID 记录,不记录反馈正文、邮箱、截图内容、
|
||
Cookie、CSRF token 或 secret。
|
||
|
||
## 数据保留
|
||
|
||
当前版本不自动删除反馈或附件。部署方应根据实际支持流程定义保留期限,
|
||
并将数据库行和对应附件视为同一条记录处理。任何直接数据库维护都必须先
|
||
停止写入、完成备份,并在隔离副本上验证。
|