Files
2026-08-24 17:59:49 +08:00

224 lines
7.6 KiB
Markdown
Raw Permalink 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.
# 部署与运维
## 配置
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。
## 数据保留
当前版本不自动删除反馈或附件。部署方应根据实际支持流程定义保留期限,
并将数据库行和对应附件视为同一条记录处理。任何直接数据库维护都必须先
停止写入、完成备份,并在隔离副本上验证。