# 部署与运维手册（与 `release/prod` 基线一致）

最后核对：2026-08-30，总工 HEL-270 上线前总验收。本手册是测试与正式环境唯一的部署/回滚/备份/巡检操作依据，取代散落在宿主机 `DEPLOYMENT.txt` 里的历史记录（旧记录仅作存档）。

## 1. 物料与目录

| 物料 | 说明 |
|---|---|
| `deploy/Dockerfile` | python:3.12-slim-bookworm，非 root（uid 10001 `app`），只读根文件系统 |
| `deploy/compose.yaml` | cap_drop ALL、no-new-privileges、pids 128、mem 512m、cpu 1.0、healthcheck |
| `deploy/.env.example` | 配置模板；`deploy/.env` 已 gitignore，口令永不入库 |
| `deploy/deploy.sh` | `TAG=<tag> ./deploy.sh`，默认镜像 tag=当前提交 sha |
| `deploy/rollback.sh` | `./rollback.sh <tag>`，只换应用不动数据 |
| `deploy/backup.sh` | 在线热备 + 保留策略（30 天日备 + 12 个月月备） |
| `deploy/monitor.sh` | 每 5 分钟健康 + 磁盘 ≥80% 告警，写 `ALERT.log` |

数据目录：`deploy/../data/`（`app.db` + `files/`）。银行原始文件按 SHA-256 内容哈希不可变保存，任何操作不得改写。

## 2. 正式配置铁律

- **不得出现 `admin/admin123`**：`APP_BOOTSTRAP_ADMIN_PASSWORD` 正式环境不设值，首启由控制台读取随机生成的一次性初始口令，首登立即改密；管理员用户名避开 `admin`。
- **登录失败限流默认开启**（5 次/10 分钟锁账号+IP）。`APP_LOGIN_RATE_LIMIT_DISABLED=1` 仅限本地调试，正式 .env 不得携带。
- 密钥/口令/证书私钥不进仓库、不进评论、不进任务元数据。
- **HTTPS**：正式暴露一律经反向代理（推荐同机 Caddy 容器，内网自签或内部 CA 证书，反代到容器 4173）；不占用 NAS 系统 nginx 的 80/443。公司电脑分发根证书即可。
- **日志**：容器 stdout 走 docker json-file，限幅 `max-size=10m, max-file=3`（daemon 或 compose `logging` 配置）；审计日志在库内（append-only 触发器保护）。

## 3. 部署步骤（测试与正式一致）

```bash
git fetch && git checkout release/prod && git pull
cd deploy
cp .env.example .env   # 首次；按上面铁律填写
# .env 必设 APP_UID/APP_GID = data/ 目录属主的 uid/gid（宿主机 `id <用户>`），
# 否则 SQLite WAL 写库报 readonly
TAG=$(git rev-parse --short HEAD) ./deploy.sh
curl -fsS http://127.0.0.1:4173/ >/dev/null && echo healthy
```

部署前手工备份（四件套 + 镜像 tag 存档）：

```bash
STAMP=$(date +%Y%m%d-%H%M%S)
tar -C .. -czf ../backups/source-$STAMP.tar.gz --exclude='../data' --exclude='../.git' .
cp .env ../backups/env-$STAMP
docker exec caiwuzongzhang-app python -c "import sqlite3; s=sqlite3.connect('/app/data/app.db'); d=sqlite3.connect('/tmp/b.db'); s.backup(d); d.close(); s.close()"
docker cp caiwuzongzhang-app:/tmp/b.db ../backups/app.db.$STAMP
```

## 3.1 正式环境部署（同宿主机隔离，2026-08-30 首次上线采用）

测试与正式同宿主机并行，靠 compose 项目名/容器名/端口三隔离：

| 项 | 测试环境 | 正式环境 |
|---|---|---|
| 目录 | `/home/leefer/caiwuzongzhang-test` | `/home/leefer/caiwuzongzhang-prod` |
| compose 项目 | `deploy`（默认） | `COMPOSE_PROJECT_NAME=caiwuzongzhang-prod` |
| 应用容器 | `caiwuzongzhang-app` | `caiwuzongzhang-prod-app` |
| 端口 | `4173`（对外） | `127.0.0.1:4174`（仅本机，反代上游） |
| HTTPS | 无 | nginx 反代 `8443`（`COMPOSE_PROFILES=tls`） |
| 访问 | http://192.168.200.36:4173/ | https://192.168.200.36:8443/ |

首次部署顺序：

```bash
git clone <repo> /home/leefer/caiwuzongzhang-prod && cd /home/leefer/caiwuzongzhang-prod
git checkout release/prod
mkdir -p data/files backups && chown -R 10001:10001 data   # 容器内 app uid
cd deploy
cp .env.example .env    # 按下述差异填写
./tls/gen-cert.sh       # 生成内部 CA + 服务器证书（SAN 含对外 IP）
TAG=<发布镜像tag> docker compose up -d    # 不带 --build，直接用已验收镜像
docker logs <应用容器> 2>&1 | grep "shown once"   # 首启一次性初始口令，只出现一次
```

正式 `.env` 与测试的差异（铁律）：

- `COMPOSE_PROJECT_NAME=caiwuzongzhang-prod`、`APP_CONTAINER_NAME=caiwuzongzhang-prod-app`、`APP_PORT_MAP=127.0.0.1:4174:4173`
- `COMPOSE_PROFILES=tls`、`PROXY_CONTAINER_NAME=caiwuzongzhang-proxy`、`PROXY_PORT_MAP=8443:8443`
- `APP_UID=10001` / `APP_GID=10001`（data/ 已 chown 10001）
- 管理员用户名自定（如 `jinniu-admin`）；**不设** `APP_BOOTSTRAP_ADMIN_PASSWORD`（首启控制台取随机一次性口令，首登立即改密）
- **不设** `APP_LOGIN_RATE_LIMIT_DISABLED`（限流默认开启）

证书与信任分发：

- `tls/certs/ca.crt` 发给各公司电脑安装到「受信任的根证书颁发机构」（安装指引见交接文档）；`ca.key`/`server.key` 永不离开宿主机。
- 服务器证书有效期 5 年，到期前用 `./tls/gen-cert.sh` 重签并 `docker compose restart proxy`。

## 4. 回滚（应用层，5 分钟内）

```bash
cd deploy && ./rollback.sh <上一个镜像tag>
```

数据层恢复见下节。两套动作互不干扰：应用回滚不改数据；数据恢复不换镜像。

## 5. 备份与恢复演练

- 备份：cron `15 2 * * *` 运行 `deploy/backup.sh`；月结后再手动跑一次。
- 恢复步骤（已演练，隔离环境验证）：
  1. `mkdir -p /tmp/restore/data && cp backups/daily/app.db.<stamp> /tmp/restore/data/app.db`
  2. `tar -C /tmp/restore/data -xzf backups/daily/files.<stamp>.tar.gz`（解出 `files/`）
  3. 用一份独立 `deploy-restore` 目录（`.env` 指向 `/tmp/restore`、端口错开）`docker compose up -d --build`
  4. 核对：`PRAGMA integrity_check`、各表行数、金额合计、`source_files` 哈希清单与生产一致后才可顶替。
- 演练频率：上线前 1 次（HEL-270 已做），之后每季度 1 次，结果记入本文件末尾。

## 6. 巡检与告警

cron `*/5 * * * *` 运行 `deploy/monitor.sh`；连续失败或磁盘 ≥80% 时查看 `deploy/ALERT.log` 并按需扩容/清理。容器重启策略 `unless-stopped`。

宿主机 crontab（leefer，2026-08-30 起，正式环境）：

```cron
15 2 * * * cd /home/leefer/caiwuzongzhang-prod/deploy && CONTAINER=caiwuzongzhang-prod-app ./backup.sh >> backup.log 2>&1
*/5 * * * * cd /home/leefer/caiwuzongzhang-prod/deploy && CONTAINER=caiwuzongzhang-prod-app HEALTH_URL=http://127.0.0.1:4174/ ./monitor.sh >/dev/null 2>&1
01 3 1 * * rsync -a /home/leefer/caiwuzongzhang-prod/backups/monthly/ /vol1/caiwuzongzhang-backups/monthly/
```

（第三行为月度异盘副本：`/vol1` 是与系统盘不同的物理卷；如后续提供真正的异机目标，改为该目标。）

## 7. 升级数据库

应用启动时自动执行迁移（`MIGRATIONS`，当前版本 10）。迁移只前不改写历史；回退 schema 用对应 down 迁移，先备份后操作。

## 8. 演练记录

- 2026-08-30 HEL-270：备份恢复演练 + 版本回滚演练各 1 次，隔离环境完成，原始证据未改动。详见 HEL-270 验收评论。
