HEL-270: 验收物料——非root加固、备份/巡检脚本、运维手册、修复 reminders 布局冒烟测试
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
+22
-13
@@ -1,23 +1,32 @@
|
||||
FROM python:3.12-slim
|
||||
FROM python:3.12-slim-bookworm
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONPATH=/app/src \
|
||||
APP_HOST=0.0.0.0 \
|
||||
APP_PORT=4173 \
|
||||
APP_DB_PATH=/app/data/app.db \
|
||||
APP_STORAGE_DIR=/app/data/files
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN groupadd --system --gid 10001 app \
|
||||
&& useradd --system --uid 10001 --gid app --home-dir /app --shell /usr/sbin/nologin app \
|
||||
&& mkdir -p /app/data \
|
||||
&& chown -R app:app /app
|
||||
|
||||
COPY server.py ./
|
||||
COPY src ./src
|
||||
COPY web ./web
|
||||
COPY --chown=app:app requirements.txt ./
|
||||
RUN python -m pip install --no-cache-dir --requirement requirements.txt
|
||||
|
||||
ENV PYTHONPATH=/app/src
|
||||
ENV APP_HOST=0.0.0.0
|
||||
ENV APP_PORT=4173
|
||||
ENV APP_DB_PATH=/app/data/app.db
|
||||
ENV APP_STORAGE_DIR=/app/data/files
|
||||
COPY --chown=app:app server.py ./
|
||||
COPY --chown=app:app src ./src
|
||||
COPY --chown=app:app web ./web
|
||||
|
||||
USER app
|
||||
|
||||
EXPOSE 4173
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:4173/', timeout=4)"
|
||||
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=5 \
|
||||
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:4173/', timeout=2)"]
|
||||
|
||||
CMD ["python", "server.py"]
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# 部署与运维手册(与 `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 # 首次;按上面铁律填写
|
||||
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
|
||||
```
|
||||
|
||||
## 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`。
|
||||
|
||||
## 7. 升级数据库
|
||||
|
||||
应用启动时自动执行迁移(`MIGRATIONS`,当前版本 10)。迁移只前不改写历史;回退 schema 用对应 down 迁移,先备份后操作。
|
||||
|
||||
## 8. 演练记录
|
||||
|
||||
- 2026-08-30 HEL-270:备份恢复演练 + 版本回滚演练各 1 次,隔离环境完成,原始证据未改动。详见 HEL-270 验收评论。
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# 在线热备:不停服备份 SQLite(backup API 保证一致性)+ files/ 原始文件目录。
|
||||
# 用法:./backup.sh [备份根目录](默认 ../backups)
|
||||
# 保留策略:日备保留 30 天,每月 1 号的首份备份额外保留 12 个月(monthly/)。
|
||||
# 建议宿主机 cron:15 2 * * * /path/to/deploy/backup.sh >> /path/to/deploy/backup.log 2>&1
|
||||
# 官方环境建议至少将 monthly/ 同步一份到异机/异盘。
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
DEST="${1:-$ROOT/../backups}"
|
||||
CONTAINER="${CONTAINER:-caiwuzongzhang-app}"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
DAY_DIR="$DEST/daily"
|
||||
MONTH_DIR="$DEST/monthly"
|
||||
mkdir -p "$DAY_DIR" "$MONTH_DIR"
|
||||
|
||||
# 1) SQLite 在线热备(容器内 python sqlite3 backup API,主库可继续写入)
|
||||
docker exec "$CONTAINER" python - <<'PY'
|
||||
import os, sqlite3
|
||||
src = sqlite3.connect(os.environ.get("APP_DB_PATH", "/app/data/app.db"))
|
||||
dst = sqlite3.connect("/tmp/app.db.backup")
|
||||
src.backup(dst)
|
||||
dst.close(); src.close()
|
||||
print("hot-backup ok")
|
||||
PY
|
||||
docker cp "$CONTAINER:/tmp/app.db.backup" "$DAY_DIR/app.db.$STAMP"
|
||||
docker exec "$CONTAINER" rm -f /tmp/app.db.backup
|
||||
|
||||
# 2) 原始文件目录打包(银行原始证据,只读复制,绝不改动)
|
||||
tar -C "$ROOT/.." -czf "$DAY_DIR/files.$STAMP.tar.gz" data/files
|
||||
|
||||
# 3) 完整性自检:PRAGMA integrity_check + 关键计数
|
||||
docker run --rm -v "$DAY_DIR/app.db.$STAMP:/check/app.db:ro" python:3.12-slim-bookworm \
|
||||
python -c "
|
||||
import sqlite3
|
||||
c = sqlite3.connect('/check/app.db')
|
||||
print('integrity:', c.execute('PRAGMA integrity_check').fetchone()[0])
|
||||
for t in ('source_rows', 'transfer_match_decisions', 'period_close_runs', 'audit_log'):
|
||||
try:
|
||||
print(t, c.execute(f'SELECT COUNT(*) FROM {t}').fetchone()[0])
|
||||
except sqlite3.OperationalError:
|
||||
print(t, 'n/a')
|
||||
"
|
||||
|
||||
# 4) 保留策略:日备 >30 天删除;每月 1 号留档 monthly/
|
||||
find "$DAY_DIR" -name 'app.db.*' -mtime +30 -delete
|
||||
find "$DAY_DIR" -name 'files.*.tar.gz' -mtime +30 -delete
|
||||
if [ "$(date +%d)" = "01" ]; then
|
||||
cp "$DAY_DIR/app.db.$STAMP" "$MONTH_DIR/app.db.$STAMP"
|
||||
cp "$DAY_DIR/files.$STAMP.tar.gz" "$MONTH_DIR/files.$STAMP.tar.gz"
|
||||
find "$MONTH_DIR" -mtime +365 -delete
|
||||
fi
|
||||
echo "backup complete: $DAY_DIR/app.db.$STAMP"
|
||||
+20
-21
@@ -1,36 +1,35 @@
|
||||
# 测试环境部署编排。正式口令不得写入本文件。
|
||||
# 使用:复制 .env.example 为 .env 后填写,再执行 ./deploy.sh
|
||||
# 测试/正式环境部署编排(正式口令不得写入本文件)。
|
||||
# 使用:复制 .env.example 为 .env 填写;TAG 指定镜像标签(默认取当前提交 sha)。
|
||||
# 例:TAG=v1.0.0-rc1 docker compose -f compose.yaml up -d --build
|
||||
|
||||
services:
|
||||
caiwuzongzhang:
|
||||
app:
|
||||
container_name: caiwuzongzhang-app
|
||||
image: caiwuzongzhang:${TAG:-latest}
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/Dockerfile
|
||||
image: caiwuzongzhang:test
|
||||
container_name: caiwuzongzhang-test
|
||||
ports:
|
||||
- "4173:4173"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
APP_HOST: "0.0.0.0"
|
||||
APP_PORT: "4173"
|
||||
APP_DB_PATH: /app/data/app.db
|
||||
APP_STORAGE_DIR: /app/data/files
|
||||
ports:
|
||||
- "4173:4173"
|
||||
volumes:
|
||||
- ../data:/app/data
|
||||
init: true
|
||||
restart: unless-stopped
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
cap_drop:
|
||||
- ALL
|
||||
- /tmp:size=64m,mode=1777
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
pids_limit: 128
|
||||
mem_limit: 512m
|
||||
restart: unless-stopped
|
||||
cpus: 1.0
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:4173/', timeout=4)"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:4173/', timeout=2)"]
|
||||
interval: 15s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
+13
-8
@@ -1,12 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# 测试环境部署。施工员不执行上线;由总工在测试机运行。
|
||||
# 部署(测试或正式环境通用)。施工员不执行上线;由总工运行。
|
||||
# 用法:TAG=<镜像标签,默认当前 git 提交 sha> ./deploy.sh
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$ROOT"
|
||||
if [[ ! -f .env ]]; then
|
||||
echo "缺少 deploy/.env。请复制 .env.example 后填写测试口令。" >&2
|
||||
exit 1
|
||||
cd "$ROOT/.."
|
||||
if [[ ! -f deploy/.env ]]; then
|
||||
echo "缺少 deploy/.env。请复制 deploy/.env.example 后填写(正式口令不得入库)。" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$ROOT/../data/files"
|
||||
docker compose -f compose.yaml up -d --build
|
||||
echo "已启动测试环境:http://127.0.0.1:4173/"
|
||||
if [[ -z "${TAG:-}" ]]; then
|
||||
TAG="$(git rev-parse --short HEAD 2>/dev/null || echo latest)"
|
||||
fi
|
||||
mkdir -p data/files
|
||||
cd "$ROOT"
|
||||
TAG="$TAG" docker compose -f compose.yaml up -d --build
|
||||
echo "已启动:http://127.0.0.1:${APP_PORT:-4173}/ 镜像 caiwuzongzhang:$TAG"
|
||||
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# 巡检:健康端点 + 磁盘水位告警。失败/越线写 ALERT.log(供总管日检)。
|
||||
# 建议 cron:*/5 * * * * /path/to/deploy/monitor.sh >/dev/null 2>&1
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
URL="${HEALTH_URL:-http://127.0.0.1:4173/}"
|
||||
DATA_DIR="${DATA_DIR:-$ROOT/../data}"
|
||||
ALERT="$ROOT/ALERT.log"
|
||||
DISK_THRESHOLD="${DISK_THRESHOLD:-80}"
|
||||
|
||||
now() { date "+%Y-%m-%d %H:%M:%S"; }
|
||||
|
||||
# 1) 健康检查:HTTP 200 才算通过
|
||||
if ! curl -fsS -m 8 -o /dev/null "$URL"; then
|
||||
echo "[$(now)] HEALTH FAIL: $URL 无响应" >> "$ALERT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2) 磁盘水位:数据所在分区使用率超阈值告警
|
||||
pct="$(df -P "$DATA_DIR" | awk 'NR==2 {gsub("%",""); print $5}')"
|
||||
if [ "${pct:-0}" -ge "$DISK_THRESHOLD" ]; then
|
||||
echo "[$(now)] DISK ALERT: $DATA_DIR 使用率 ${pct}% >= ${DISK_THRESHOLD}%" >> "$ALERT"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# 3) 容器状态:非 running/restarting 告警
|
||||
state="$(docker inspect -f '{{.State.Status}}' "${CONTAINER:-caiwuzongzhang-app}" 2>/dev/null || echo missing)"
|
||||
if [ "$state" != "running" ]; then
|
||||
echo "[$(now)] CONTAINER ALERT: ${CONTAINER:-caiwuzongzhang-app} state=$state" >> "$ALERT"
|
||||
exit 3
|
||||
fi
|
||||
echo "[$(now)] ok"
|
||||
+13
-9
@@ -1,15 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# 测试环境回滚:停掉当前容器,按 TAG 或上一个镜像再拉起。
|
||||
# 用法:./rollback.sh [image-tag]
|
||||
# 回滚:回到指定镜像标签(或最近一次可用镜像)。
|
||||
# 用法:./rollback.sh <tag> 例:./rollback.sh f5e0915
|
||||
# 数据库与 files/ 原始证据不受影响;如需回退数据,见 deploy/README.md「数据恢复」。
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$ROOT"
|
||||
TAG="${1:-}"
|
||||
docker compose -f compose.yaml down
|
||||
if [[ -n "$TAG" ]]; then
|
||||
export COMPOSE_IMAGE="caiwuzongzhang:${TAG}"
|
||||
docker compose -f compose.yaml up -d
|
||||
echo "已回滚到镜像 caiwuzongzhang:${TAG}"
|
||||
else
|
||||
echo "已停止测试容器。指定镜像标签可重新拉起:./rollback.sh <tag>"
|
||||
if [[ -z "$TAG" ]]; then
|
||||
echo "用法:./rollback.sh <镜像tag>(可用 tag 见 docker images caiwuzongzhang)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! docker image inspect "caiwuzongzhang:$TAG" >/dev/null 2>&1; then
|
||||
echo "镜像 caiwuzongzhang:$TAG 不存在。" >&2
|
||||
exit 1
|
||||
fi
|
||||
TAG="$TAG" docker compose -f compose.yaml down
|
||||
TAG="$TAG" docker compose -f compose.yaml up -d
|
||||
echo "已回滚到 caiwuzongzhang:$TAG"
|
||||
|
||||
Reference in New Issue
Block a user