本文档描述 EventShock 的 GitHub 拉取式发布链路。目标服务器是 Ubuntu 22.04 x86_64,公网 IPv4 为 47.251.41.145,正式访问地址为 https://eventshock.mikezhuang.cn。
正常发布必须经过以下路径,不再把开发机的未提交工作区直接复制到生产服务器:
本地功能分支完成测试与前端构建
-> commit 并 push 到 GitHub
-> 通过 Pull Request 合入 main
-> GitHub 三项必需 CI 全部通过
-> 宝塔原生计划任务每 10 分钟运行一次
-> 服务器匿名 fetch 公有仓库
-> 校验快进关系并按 commit 执行 git archive
-> 构建带唯一标签的应用镜像
-> 容器与公网健康检查均返回目标 commit SHA
-> 切换 /opt/eventshock/current;失败则恢复上一版本
当前部署分支固定为 main。所有代码先在个人 codex/* 功能分支开发并通过 Pull Request 合入;不要直接向 main 推送,也不要通过修改服务器文件绕过 GitHub 与 CI。
首次引导时由 Caddy 直连应用;完成本指南第 9 节后,正式生产拓扑如下:
Internet :80/:443
-> Caddy(公网 TLS、安全响应头、压缩)
-> host.docker.internal:18080(宝塔 Nginx,仅 Docker 私网监听)
-> 127.0.0.1:18000(EventShock app)
caddy是默认且唯一的公网入口,负责证书申请与续期。app同时提供 FastAPI 与已构建的 React 单页应用。它只映射到宿主机回环地址127.0.0.1:18000,不能从公网直接访问。- 宝塔 Nginx 是真实的中间反向代理而不是面板占位记录;生产请求确实经过其 access log,供宝塔站点流量统计使用。
- Caddy 没有配置会持续写入 Nginx access log 的后台主动探针。正常重启时,Caddy 容器先等待应用健康,再等待最终代理链健康,之后才监听公网端口;默认等待上限为 90 秒,永久故障超时后仍启动 Caddy,以保留 TLS 与错误可观测性。运行期间则对连接失败执行最多 5 秒、间隔 250 毫秒的请求内重试。这样既不会把正常应用启动窗口暴露成 502,也避免系统探针主导宝塔访问计数。
- UFW 启用时,注册工具只允许动态识别出的 Caddy Docker bridge 及其 subnet 访问 host-gateway 的
18080;该规则是容器到宿主机的内部通道,不是公网端口放行。 - 实验状态接口使用 SSE;宝塔 Nginx 的 EventShock 代理配置必须关闭响应缓冲,保留长连接相关请求头,并且不能缓存
/api/v1/experiments/*/events。 - SQLite 保存在 Docker 命名卷
eventshock-data;重新构建应用镜像不会删除该卷。 - 最小部署证据也保存在同一持久卷的
/data/deployment-status.json。同步脚本在宿主机侧原子替换该文件,应用只通过治理 GET 接口读取,不提供写接口;容器重启或镜像切换不会删除它。 - 长期认证随机密钥、SMTP 密码与管理员 API 凭据加密主密钥只保存在服务器
/opt/eventshock/shared/secrets,以只读文件挂载给固定 UID10001的应用;真实值不进入 Git、Compose 环境变量、镜像或部署日志。管理员实际供应商 API Key 不放在该目录,而是由应用以该独立主密钥加密后仅将密文写入 SQLite。 - 每个发布目录都生成独立的
.release.env,应用镜像标签形如eventshock-app:<release-id>。上一版本不会因下一次构建而被同名镜像覆盖。
第 9 节说明如何在不中断现有 HTTPS 的前提下完成宝塔 Nginx 接入。Caddy 直连仅作为首次引导和故障诊断模式,不是本项目要求的最终监测拓扑。
在域名服务商的 DNS 控制台为 mikezhuang.cn 新增或确认:
| 字段 | 填写内容 |
|---|---|
| 记录类型 | A |
| 主机记录 / Name | eventshock |
| 记录值 / Value | 47.251.41.145 |
| TTL | 600 或 Auto |
| 路由线路 | 默认 |
不要在记录值中填写 https://、路径或端口。当前没有确认可用且稳定的公网 IPv6,因此不要添加 AAAA 记录。可复核:
dig +short eventshock.mikezhuang.cn A @1.1.1.1
dig +short eventshock.mikezhuang.cn A @8.8.8.8两条命令都应输出:
47.251.41.145
阿里云安全组入方向还必须允许:
| 协议 | 端口 | 来源 |
|---|---|---|
| TCP | 80 |
0.0.0.0/0 |
| TCP | 443 |
0.0.0.0/0 |
UDP 443 只用于可选 HTTP/3;不开放不会影响 TCP HTTPS。不要在云安全组中放行应用端口 18000 或 Nginx 内部端口 18080。UFW 可以存在一条由注册工具管理的精确规则:来源必须是动态识别的 Caddy Docker subnet,入接口必须是对应 Linux bridge,目标必须是 host-gateway 且端口只能是 18080/tcp;不得存在面向 Anywhere、0.0.0.0/0 或其他公网来源的 18080 规则。SSH 与宝塔面板端口继续遵循服务器现有访问限制。
在项目根目录确认当前位于本次任务的个人功能分支,并先同步远程引用:
cd /Users/mike/Documents/University/Grade_1B/UCB_Summer_Session/Classes/ENGIN_170E/Project/repo
git switch codex/<your-feature-branch>
git fetch origin
git status --short执行后端检查:
conda activate eventshock
python -c "import sys; print(sys.executable); assert sys.version_info[:3] == (3, 12, 13)"
python -m ruff check backend tests
python -m ruff format --check backend tests
python -m pytest执行前端检查并生成发布工件:
cd frontend
npm ci
npm run typecheck
npm test
npm run build
cd ..frontend/dist 是发布输入,必须与源码一起受 Git 跟踪。GitHub 的前端任务会重新构建并执行 git diff --exit-code -- dist;源码与已提交工件不一致时,CI 会失败,服务器不会部署。提交时必须包含本次源码和对应的 frontend/dist,但不要把密钥、.env、数据库、缓存或无关文件加入提交:
git status --short
git add \
.dockerignore .env.example .github .gitignore .python-version \
Caddyfile Dockerfile README.md backend compose.yml docs \
environment.yml event-packs frontend pyproject.toml \
requirements-dev.lock requirements.lock scripts tests usage_documents
git diff --cached --check
git diff --cached --stat
git commit -m "feat: describe the completed change"
git push --set-upstream origin HEAD确认本地 HEAD 与当前功能分支的 GitHub upstream 一致;随后创建 Pull Request 并合入 main:
git fetch origin
git rev-parse HEAD
git rev-parse '@{upstream}'两个 SHA 应完全相同。禁止直接向 main 推送;评审与合并流程见 Git 使用指南。
服务器只接受下列三个 GitHub Check Run 全部以 success 完成的目标提交:
Backend / Python 3.12.13Frontend / Node 22Production container
同步脚本通过 GitHub 公共 API 查询公有仓库,不保存 GitHub Token。同名检查若存在多条,必须全部成功,不能用一条成功结果掩盖另一条失败或未完成结果。检查尚未完成或数量不足时记录 WAIT_CI 并等待下一轮;任一检查失败时记录 CI_BLOCKED 并停止,不改动当前服务。
GitHub 页面上的检查结果才是该提交的远程门禁证据。本地测试通过不能代替这三项检查,服务器也不会通过跳过检查来“抢先”部署。
同步脚本把这三项检查的逐项状态写入持久化部署证据。只有 GitHub API 对目标 SHA 返回已完成且结论为 success 时才写 PASS;缺失或未完成写 PENDING,失败结论写 FAIL,API 本身不可验证时写 UNKNOWN。状态文件只包含部署 SHA、GitHub main SHA、分支、三项检查、同步/部署时间、结果和受限失败码,不包含日志正文、HTTP 响应、Token、Cookie 或密钥。
本节用于当前服务器已有可用 EventShock 发布目录的情况。安装脚本把同步入口、宝塔任务包装器和宝塔注册工具安装到不可变运维版本目录,并让 /opt/eventshock/bin 原子指向该目录;同时创建 root 专用配置。它不会直接写系统 crontab。
ssh sv
sudo bash /opt/eventshock/current/scripts/install-github-sync.sh /opt/eventshock/current
sudo sed -n '1,120p' /opt/eventshock/shared/github-sync.env默认配置为:
EVENTSHOCK_GITHUB_URL=https://github.com/Mike-Zhuang/EventShock.git
EVENTSHOCK_GITHUB_BRANCH=main
EVENTSHOCK_GITHUB_REPOSITORY=Mike-Zhuang/EventShock配置文件位于 /opt/eventshock/shared/github-sync.env,由 root 拥有且权限为 0600。新安装默认跟踪 main;安装器不会覆盖已经存在的配置,因此旧服务器升级后必须人工确认该值。仓库为公有仓库,配置中不应出现 GitHub Token、密码或部署密钥。
真实密码不得写入仓库、.env.example、Docker 环境变量或 SSH 命令参数。先建立受限目录;应用固定使用 UID/GID 10001,最终只允许读取三个长期密钥文件。先人工配置认证与 SMTP 两项:
sudo install -d -o root -g 10001 -m 0750 /opt/eventshock/shared/secrets
openssl rand -hex 32 \
| sudo install -o root -g 10001 -m 0440 /dev/stdin \
/opt/eventshock/shared/secrets/auth-secret
read -r -s -p 'SMTP password: ' smtpPassword
printf '\n'
printf '%s\n' "${smtpPassword}" \
| sudo install -o root -g 10001 -m 0440 /dev/stdin \
/opt/eventshock/shared/secrets/smtp-password
unset smtpPassword第三个文件 admin-api-key-encryption-key 是用于 Fernet 认证加密的 32 字节 URL-safe Base64 主密钥,不是供应商 API Key。deploy-server.sh 会在文件不存在时从 /dev/urandom 幂等生成 44 字节密钥,通过同目录硬链接原子安装为 root:10001 0440,并在每次部署前严格验证;已存在的文件(包括不安全的符号链接)绝不会被自动覆盖。不要手工删除或替换已经用于加密数据的主密钥。
在 root 专用的 /opt/eventshock/shared/.env 中只填写非敏感配置和密钥目录路径;以下值都是占位示例:
EVENTSHOCK_AUTH_REQUIRED=true
EVENTSHOCK_AUTH_COOKIE_SECURE=true
EVENTSHOCK_DEPLOYMENT_STATUS_FILE=/data/deployment-status.json
EVENTSHOCK_ADMIN_EMAIL=admin@example.com
EVENTSHOCK_SMTP_HOST=smtp.example.com
EVENTSHOCK_SMTP_PORT=465
EVENTSHOCK_SMTP_USERNAME=sender@example.com
EVENTSHOCK_SMTP_SENDER=sender@example.com
EVENTSHOCK_SECRETS_DIR=/opt/eventshock/shared/secrets旧服务器的共享 .env 不会被安装器覆盖,升级时必须人工补入上述固定部署状态路径;主密钥文件由新版部署脚本在首次升级时生成并验证,不需要把主密钥或其容器路径写入共享 .env。主密钥的宿主机路径固定为 /opt/eventshock/shared/secrets/admin-api-key-encryption-key,Compose 在容器内固定设置 EVENTSHOCK_ADMIN_API_KEY_ENCRYPTION_KEY_FILE=/run/secrets/eventshock/admin-api-key-encryption-key。文件缺失、权限不安全或格式错误都会安全终止发布。
首次发布还需要一个只允许 root 读取的一次性管理员初始密码。它不挂载为应用可读密钥,也不设置成环境变量;新版本通过容器健康检查后,部署脚本用标准输入把它交给管理员引导命令,事务成功后立即删除:
read -r -s -p 'Initial admin password: ' adminPassword
printf '\n'
printf '%s\n' "${adminPassword}" \
| sudo install -o root -g root -m 0400 /dev/stdin \
/opt/eventshock/shared/secrets/admin-bootstrap-password.once
unset adminPassword只核对文件名、所有者和权限,不得打印内容:
sudo find /opt/eventshock/shared/secrets -maxdepth 1 -type f \
-printf '%u:%g %m %f\n' | sort预期目录为 root:10001 750,使容器 UID/GID 10001 只能进入受限目录;auth-secret、smtp-password、admin-api-key-encryption-key 为 root:10001 440,一次性管理员文件为 root:root 400,因此应用即使看见文件名也不能读取初始密码,且整个挂载在容器内为只读。部署脚本会在构建和启动前逐项验证这些所有者、权限、文件类型、大小及共享环境配置,任何偏差都会终止发布。管理员引导失败时部署会回滚并保留 .once 文件以便安全重试;成功时该文件必须消失。不要在共享配置中增加 EVENTSHOCK_ADMIN_BOOTSTRAP_PASSWORD 或 EVENTSHOCK_ADMIN_BOOTSTRAP_PASSWORD_FILE。
admin-api-key-encryption-key 必须进入受控的主机 Secret 备份,并与 SQLite 备份分开保管。丢失该主密钥后,现有 auth_persistent_llm_credentials 密文无法恢复;泄露该主密钥时,应立即停止外部模型调用、轮换供应商 API Key 与主密钥,并删除或重新加密旧密文。主机 root、Docker 管理员和应用进程能够读取或使用主密钥,仍属于明确的信任边界;本设计不应被描述为“只有管理员本人技术上可知”。
安装完成后,可先做只读检查:
sudo /opt/eventshock/bin/register-baota-task.py --show
sudo readlink -f /opt/eventshock/bin
sudo ls -l /opt/eventshock/bin/baota-eventshock-task.sh
sudo ls -l /opt/eventshock/bin/sync-from-github.sh对于一台完全空白的新服务器,仍需要先从一个已评审的 Git commit 引导运行一次 scripts/deploy-server.sh,建立 Docker、共享配置和初始发布目录;此后所有常规更新必须走 GitHub 拉取链路。不要用本地未提交工作区作为引导源码。
为了让任务在宝塔“计划任务”前端及其“任务日志”中原生可见,必须调用宝塔自身的 crontab().AddCrontab,不能只在 /etc/crontab 中手写一行。本仓库提供的注册工具通过宝塔 10.x 自身的 Python 环境和 AddCrontab 完成这一操作。
先查看是否已有同名任务:
sudo /opt/eventshock/bin/register-baota-task.py --show没有任务时注册;若同名任务存在但字段不同,人工核对后才使用 --replace:
sudo /opt/eventshock/bin/register-baota-task.py
# 仅在已核对同名旧任务可以被替换时执行:
sudo /opt/eventshock/bin/register-baota-task.py --replace注册结果应满足:
- 名称:
EventShock GitHub 自动同步部署 - 类型:Shell 脚本
- 周期:每
10分钟 - 任务入口:
/opt/eventshock/bin/baota-eventshock-task.sh - 状态:启用
随后在宝塔网页的“计划任务”中核对名称、周期和启用状态。需要立即验证一次时,通过宝塔执行已注册任务:
sudo /opt/eventshock/bin/register-baota-task.py --run--run 不是绕过宝塔直接执行 shell:注册工具调用宝塔自身的任务执行入口,随后用 GetLogs 读取同一条任务的原生日志。命令结果中的 logsReadableInPanel=true 只能证明宝塔接口能读到日志;仍应在面板“计划任务 → 日志”中人工核对一次名称、执行时间、目标 SHA 和最终状态。
不要另建第二条系统 cron 或重复的宝塔任务。同步脚本通过 flock 防止前一轮尚未结束时重入,但重复调度会制造不必要的日志和 GitHub API 请求。
包装器使用 tee 同时保留两份输出,并通过 PIPESTATUS 传播真实退出码:
- 宝塔原生任务日志:在宝塔“计划任务”中点击该任务的“日志”查看。
- 稳定审计日志:
/opt/eventshock/shared/logs/github-sync.log。
SSH 查看稳定日志:
sudo tail -n 200 /opt/eventshock/shared/logs/github-sync.log
sudo grep -E 'NO_CHANGE|WAIT_CI|CI_BLOCKED|RUNTIME_(DRIFT|SELF_HEAL_)|TARGET_SELF_HEAL|INFRASTRUCTURE_BLOCKED|DEPLOY_(START|SUCCESS)|ERROR' \
/opt/eventshock/shared/logs/github-sync.log | tail -n 100安装脚本同时写入 /etc/logrotate.d/eventshock-github-sync,按天轮转并保留 14 份压缩日志。是否已在特定服务器成功注册、执行和显示,应以 --show、宝塔网页与上述实际日志为准;仅存在脚本不等于注册已经完成。
/opt/eventshock/bin/sync-from-github.sh 每次运行会:
- 使用非阻塞文件锁,避免两轮部署并发执行。
- 在访问 GitHub 前核对 current 发布、容器环境与公网健康 SHA;发生漂移时先调用当前注册器自愈 Caddy→宝塔 Nginx→应用链路,因此 GitHub 临时不可达不会阻止本机恢复。
- 以匿名 HTTPS 初始化或更新裸仓库镜像
/opt/eventshock/shared/github-mirror.git,并把main抓取到固定内部引用;不在生产目录执行git pull或合并。 - 当前运行时、自愈结果、同步状态和目标 SHA 全部一致时才输出
NO_CHANGE。 - 要求上次已部署提交是目标提交的祖先;发现 force-push、rebase 或其他非快进历史时拒绝部署。
- 查询目标 SHA 的三项 GitHub CI;只有全部成功才继续。
- 使用
git archive把该提交解包到临时目录,拒绝缺少frontend/dist/index.html、后端入口、注册器或 systemd 安装器的提交。旧注册器无法恢复代理但 GitHub 存在新目标时,会先用已经通过 CI 的目标注册器再自愈一次,避免修复版本被旧故障锁死。 - 本地应用健康但代理仍无法恢复时记录
INFRASTRUCTURE_BLOCKED,不把已知正常的已部署 commit 写入失败退避;只有需要实际发布的目标才进入发布失败退避。 - 运行目标提交自己的
deploy-server.sh,创建/opt/eventshock/releases/<release-id>,验证认证配置和密钥权限,并构建唯一镜像标签eventshock-app:<release-id>。 - 如果已有线上版本,先用目标提交的注册器在旧应用仍运行时安装并验收 EventShock 专用的无 Cookie 宝塔流量格式;该步骤完成前不得启动认证版本。
- 等待新应用容器通过内部健康检查。首次管理员尚未创建的短暂窗口内,后端会对注册、登录和密码重置返回
AUTHENTICATION_INITIALIZING,因此不能抢先创建普通账号。 - 若 root 专用的一次性管理员密码文件存在,通过容器标准输入执行幂等管理员引导;成功后删除文件,失败则触发整次发布回滚。
- 在容器内确认配置的管理员确实存在,且六类历史业务记录的未归属数量全部为零;遗漏
.once文件或迁移不完整都会阻断发布。 - 再次修复并验证宝塔代理,要求公网
/api/health同时返回status=ok与目标 40 位releaseCommit。 - 在 CI 通过、等待、失败、部署成功和
NO_CHANGE分支原子更新eventshock-data卷中的deployment-status.json。NO_CHANGE只在 SHA 一致时沿用同一目标的既有三项PASS证据,并更新同步时间;没有可验证旧证据时保持UNKNOWN,绝不补写PASS。 - 成功后才写传统同步状态,并把
/opt/eventshock/bin原子切换到目标 commit 的不可变运维脚本目录;失败时先验证宝塔代理和上一版本公网 SHA,再恢复上一发布目录。部署证据文件为0644、不可由组或其他用户写入,应用 UID10001可读。
同一目标 SHA 部署失败后会进入有界退避:第一次至少等待 30 分钟,后续失败至少等待 60 分钟;分支出现新的 SHA 后自动解除。这样 10 分钟计划任务仍会持续记录状态,但不会反复构建一个已知失败版本。应用发布和运维脚本各保留最近 5 个不可变版本,当前版本不会被清理。
部署脚本会记录切换前的发布目录。新版本构建、启动、容器健康或公网 SHA 校验失败时,退出陷阱会把 /opt/eventshock/current 恢复为上一发布目录,并重新启动该目录对应的唯一镜像。SQLite 与 Caddy 证书卷不会随代码发布删除。
查看当前版本和服务状态:
ssh sv
sudo readlink -f /opt/eventshock/current
sudo cat /opt/eventshock/shared/github-sync.state
sudo /opt/eventshock/current/scripts/compose-current.sh ps
curl --fail --show-error https://eventshock.mikezhuang.cn/api/health
curl --fail --show-error \
https://eventshock.mikezhuang.cn/api/v1/governance/deployment-status | jq治理接口以运行中进程的 releaseCommit 为权威,并把持久证据仅作为 CI、GitHub main 和同步结果的补充。commitAlignment 不是 MATCH、requiredChecksStatus 不是 PASS,或 statusFileState 不是 VERIFIED 时,不得声称生产版本与 GitHub 已完全对齐。文件异常会返回受限错误码而不会回显原始内容。
查看容器日志:
sudo /opt/eventshock/current/scripts/compose-current.sh logs --tail=100 app
sudo /opt/eventshock/current/scripts/compose-current.sh logs --tail=100 caddy不要执行 compose-current.sh down --volumes,否则会删除 SQLite 与 Caddy 持久数据。不要手工改写 github-sync.state 来跳过非快进保护;发生历史重写时应先停止任务、查明 GitHub 分支和当前部署关系,再制定人工恢复方案。
新安装默认轮询稳定分支 main。旧服务器的安装器不会覆盖现有 root 配置;只有在代码已通过 Pull Request 合入目标稳定分支、该分支三项 CI 全绿且当前部署 SHA 是其祖先后,才可确认或修改配置:
ssh sv
sudoedit /opt/eventshock/shared/github-sync.env标准配置应为:
EVENTSHOCK_GITHUB_URL=https://github.com/Mike-Zhuang/EventShock.git
EVENTSHOCK_GITHUB_BRANCH=main
EVENTSHOCK_GITHUB_REPOSITORY=Mike-Zhuang/EventShock修改后先查看任务与配置,再通过宝塔立即执行一次:
sudo /opt/eventshock/bin/register-baota-task.py --show
sudo /opt/eventshock/bin/register-baota-task.py --run
sudo tail -n 200 /opt/eventshock/shared/logs/github-sync.log如果日志出现非快进拒绝,不要删除镜像、发布目录或状态文件。先核对:
git --git-dir=/opt/eventshock/shared/github-mirror.git log --oneline --decorate --all -n 20
sudo cat /opt/eventshock/shared/github-sync.stateEventShock 不是 PHP 应用。仅向宝塔数据库伪造一条“PHP 项目”记录既不会产生真实流量统计,也可能让面板生成占用 80/443 的站点配置,与 Caddy 冲突。因此不得直接修改宝塔数据库或伪造站点。
首次引导链路是 Caddy -> app:8000,此时宝塔 Nginx 不在请求路径中,无法根据自己的 access log 统计 EventShock 流量。最终部署必须通过宝塔受支持接口创建真实反向代理,并保持以下拓扑:
Internet :80/:443
-> Caddy
-> host.docker.internal:18080(宝塔 Nginx,仅内部监听)
-> 127.0.0.1:18000(EventShock app)
安全约束:
- Caddy 继续独占公网 TCP 80/443 并负责 TLS,不能让宝塔 Nginx 再监听这两个端口。
- Nginx 的
18080只绑定 Docker host-gateway 可达的宿主机内部地址,云安全组不得开放该端口。 - UFW 启用时,只允许 Caddy 当前所属 Docker bridge 的 subnet 经该 bridge 到 host-gateway 的
18080/tcp;禁止ufw allow 18080、Anywhere、0.0.0.0/0或其他 broad/public 规则。 - Nginx 上游只能使用
http://127.0.0.1:18000;应用端口仍保持回环绑定。 - 宝塔站点 access log 必须确实接收到 Caddy 转发的请求,面板流量数据才有意义。
完成并验证 Nginx 内部反向代理后,在 /opt/eventshock/shared/.env 设置:
EVENTSHOCK_APP_HOST_PORT=18000
CADDY_UPSTREAM=host.docker.internal:18080然后重新应用当前发布配置并检查整个链路:
sudo /opt/eventshock/current/scripts/compose-current.sh up -d --remove-orphans
curl --fail --show-error http://127.0.0.1:18000/api/health
curl --fail --show-error https://eventshock.mikezhuang.cn/api/health
sudo /opt/eventshock/current/scripts/compose-current.sh logs --tail=100 caddy宝塔 Nginx 的具体监听地址必须以目标服务器实际的 Docker host-gateway 为准,可先查询:
sudo /opt/eventshock/current/scripts/compose-current.sh exec caddy \
getent hosts host.docker.internal目标服务器尚未安装宝塔 Nginx 时,先通过宝塔官方软件入口安装;当前宝塔 10.x 的 CLI 极速安装命令为:
优先在宝塔“软件商店”中安装 Nginx 1.30。宝塔 10.0.2 的命令行
bt install/1/nginx/1.30 存在参数解析缺陷;需要通过 SSH 自动化同一官方安装器时,
使用该版本面板实际调用的底层入口:
sudo bash -lc \
'cd /www/server/panel/install && bash ./install_soft.sh 1 install nginx 1.30'安装器会尝试创建公网 listen 80 并启动 Nginx,而 Caddy 已占用该端口,因此安装期间首次启动可能失败。不要停止或改写 Caddy;安装完成后立即使用仓库提供的注册工具创建端口 18080、PHP 版本 00 的真实宝塔站点和反向代理,并把 Nginx listener 限制到上一步查询出的私有 host-gateway:
gatewayAddress="$(
sudo /opt/eventshock/current/scripts/compose-current.sh exec -T caddy \
getent hosts host.docker.internal | awk 'NR == 1 {print $1}'
)"
sudo /opt/eventshock/bin/register-baota-site.py \
--listen-address "${gatewayAddress}"注册工具调用宝塔自己的 panelSite.AddSite 与 CreateProxy,不会直接插入 sites 数据库;它还会停用宝塔默认公网 vhost 与 phpfpm_status vhost,把安装器内置的 phpMyAdmin 888 listener 收口到 127.0.0.1:888,并要求唯一非回环业务 listener 是私网 18080。生成的站点扩展配置包含 proxy_buffering off,使实验 SSE 状态流不会被 Nginx 聚合后才一次性返回。站点会以 project_type=PHP、version=00 出现在宝塔 PHP 项目列表,但实际业务是指向 FastAPI 的反向代理,不会安装或执行 PHP。
宝塔 free_site_total 的全局默认格式会采集 $http_cookie,不能直接用于登录后的 EventShock 流量。注册工具会从宝塔当前格式生成一个站点专用副本,将 Cookie、Authorization、CSRF Token 和旧会话头固定替换为 [REDACTED],再把 EventShock 的统计 socket 切换到该格式。专用格式和站点统计配置都纳入 Nginx 快照、nginx -t 与失败回滚;不得为恢复统计而改回全局 site_total 格式。Caddy 访问日志也会显式删除相同认证头。
工具从正在运行的 Caddy 容器动态读取 Docker network ID、subnet 与对应 Linux bridge。UFW active 时,它只认领完整语法、Docker bridge、当前 host-gateway、18080/tcp 和 EventShock-Caddy-Nginx 注释都满足安全边界的规则;先添加当前窄规则,再删除旧 bridge 的自有窄规则,并能从“新旧规则同时存在”的中断状态继续收敛。任何 broad/public、错误目标、错误接口或未知规则都会在首次写操作前被拒绝。后续步骤失败时,规则按修改前的集合恢复。
注册器还会原子安装 /etc/systemd/system/nginx.service.d/eventshock-docker-order.conf:Wants=docker.service 与 After=docker.service 确保开机时 Docker 先就绪;Nginx 启动失败后每 5 秒重试,并受 60 秒内 6 次的启动限制保护。这里有意不用 Requires 或 PartOf,避免停止 Docker 时连带停止承载其他宝塔站点的全局 Nginx。安装器会启用 nginx.service,验证 drop-in 已被 systemd 读取,并在写入或验证失败时恢复原文件。
Docker 守护进程在主机重启时会并行恢复已有容器,不会重新执行 Compose 的 depends_on。因此 Caddy 使用独立的 POSIX sh 启动门控:先等待容器网络内的应用 /api/health,再携带正式域名 Host 头等待宝塔 Nginx 的 /api/health,两者都成功后才 exec caddy run 并监听 80/443。等待期间 Caddy 的 Admin API 尚未启动,容器健康检查不会把“配置可解析但服务未监听”误报为健康;停止容器时,门控也会响应 SIGTERM。共享配置可用 CADDY_STARTUP_GATE_TIMEOUT_SECONDS 调整默认 90 秒上限;超时后门控会明确记录日志并启动 Caddy,而不是永久隐藏故障。
最后,工具运行 nginx -t,由 systemd 统一接管启动状态,先检查宿主机内部 /api/health,再从 Caddy 容器内请求 http://host.docker.internal:18080/api/health。若注册过程失败,它会恢复 Nginx 配置快照、UFW 规则集合和入口时的 Nginx 运行状态。工具同时通过 SiteTotalConfig.one_site_status 启用该站点的 free_site_total,并要求服务、Unix socket、站点扩展配置和三次真实请求计数全部通过后才报告成功。
只有上述内部健康检查通过后,才把共享配置的 CADDY_UPSTREAM 改为 host.docker.internal:18080 并重新应用 Compose。最后确认:
sudo ss -ltnp | grep -E ':(80|443|18000|18080)\b'
sudo /www/server/nginx/sbin/nginx -t
sudo systemctl show nginx.service \
--property=ActiveState,After,Wants,Restart,RestartUSec,DropInPaths
sudo ufw status numbered
sudo /opt/eventshock/current/scripts/compose-current.sh exec -T caddy \
wget -q -O- -T 8 --header='Host: eventshock.mikezhuang.cn' \
http://host.docker.internal:18080/api/health
curl --fail --show-error https://eventshock.mikezhuang.cn/api/health
sudo tail -n 20 /www/wwwlogs/eventshock.mikezhuang.cn.log
sudo find /www/server/site_total/data/total -type f -mmin -10 -print
sudo /opt/eventshock/bin/register-baota-site.py \
--listen-address "${gatewayAddress}" --show验收时必须看到 Caddy 独占公网 80/443、应用只监听 127.0.0.1:18000、宝塔 Nginx 的业务入口只监听私有 host-gateway 的 18080;若保留安装器内置 phpMyAdmin listener,它必须只绑定 127.0.0.1:888。若 UFW active,状态中必须存在来源为 Caddy subnet、入接口为对应 Docker bridge、目标为 host-gateway 18080/tcp 且注释为 EventShock-Caddy-Nginx 的精确规则,同时不得存在任何 Anywhere 或其他 broad/public 18080 规则。Caddy 容器内健康请求与公网健康请求都必须成功;一次公网请求后,宝塔 access log / site_total 文件还必须实际增长。仅凭 ss 显示私网监听,不能单独证明端口没有被防火墙对公网放行。
不要在未确认内部监听、端口冲突和完整请求路径前启用该模式。是否已经产生真实统计,应同时核对 Nginx access log 与宝塔网页,不能只看站点列表中是否出现名称。
持久配置位于:
/opt/eventshock/shared/.env
默认值使用正式域名:
APP_DOMAIN=eventshock.mikezhuang.cn
APP_ENV=production
LOG_LEVEL=INFO
EVENTSHOCK_AUTH_REQUIRED=true
EVENTSHOCK_AUTH_COOKIE_SECURE=true
EVENTSHOCK_SECRETS_DIR=/opt/eventshock/shared/secrets若 DNS 或证书签发临时不可用,可短期改为:
APP_DOMAIN=http://47.251.41.145修改后执行:
sudo /opt/eventshock/current/scripts/compose-current.sh up -dIP 回退没有 HTTPS,只用于排查;问题解决后必须恢复域名。自动部署的公网 SHA 健康检查依赖 APP_DOMAIN,错误的域名配置会使新版本安全失败并触发回滚。
app最多使用 1.5 个 CPU、1024 MiB 内存;caddy最多使用 0.25 个 CPU、128 MiB 内存。- 容器采用
restart: unless-stopped与日志轮转。应用以非 root 用户运行、根文件系统只读,并删除不需要的 Linux capabilities。 - 宝塔 Nginx 的持久 systemd drop-in 要求
After/Wants=docker.service并启用失败重试;10 分钟同步任务继续核对进程、内部代理和公网 SHA,覆盖运行期崩溃与配置漂移。 - Caddy 将请求体限制为 2 MiB,并删除访问日志中的 Cookie、Authorization、CSRF Token 与旧匿名会话请求头;宝塔流量统计使用同样的站点专用脱敏格式。
- Caddy 与 Python 基础镜像锁定到仓库中审核过的版本或 digest;依赖分别由
package-lock.json、requirements.lock和requirements-dev.lock固定。 - GitHub 同步使用匿名只读请求;服务器不保存 GitHub Token。
- 每次更新前通过 SQLite online backup 在持久卷的
deployment-backups/中创建一致性备份,并只保留最近 3 份。失败回滚不会自动覆盖数据库,因为新版本短暂对外期间可能已经产生合法写入;如需恢复数据,应先停写并人工核对备份时间点。 - 在线备份使用临时数据库并在原子替换后清理该临时文件及其
-wal、-shmsidecar;不得通过通配符删除其他发布正在使用的文件。认证所有权迁移前还应额外建立一份固定名称、不会被三份滚动窗口删除的人工备份,并执行PRAGMA quick_check。 - 代码发布目录与唯一镜像默认保留最近 5 个版本,同时始终保留当前版本和直接回滚目标;清理失败只记录警告,不影响已经验证成功的版本。
- 服务器是单实例 MVP,不提供高可用。至少在重要演示前使用 SQLite 在线备份能力创建站外备份;不要在应用写入期间直接复制数据库文件。
安装 GitHub 同步入口时会同时安装并启用
eventshock-restart-verification.service。该单元平时因不存在待验证状态而跳过;
只有负责人先在维护窗口准备证据后,才会在下一次开机自动执行验证。
先创建一个只允许 root 读取的临时 Netscape Cookie 文件。它应对应专门的测试账号, 且账号至少拥有一条已完成实验。Cookie 值不会被复制进证据文件,验证完成后应立即删除:
sudo chown root:root /root/eventshock-restart.cookies
sudo chmod 0600 /root/eventshock-restart.cookies
sudo /opt/eventshock/bin/verify-restart-recovery.sh prepare \
--cookie-file /root/eventshock-restart.cookies \
--experiment-id exp-xxxxxxxxxxxxxxxxprepare 默认不会重启。它只记录当前 release SHA、宝塔 access log 与
site_total 基线,并验证 Cookie 文件权限。核对当前没有课堂演示或运行中实验、已经
进入维护窗口后,才允许使用下面的双重确认:
sudo env \
EVENTSHOCK_REBOOT_CONFIRMATION=REBOOT_EVENTSHOCK_PRODUCTION_IN_MAINTENANCE_WINDOW \
/opt/eventshock/bin/verify-restart-recovery.sh prepare \
--cookie-file /root/eventshock-restart.cookies \
--experiment-id exp-xxxxxxxxxxxxxxxx \
--reboot开机后,systemd 会在 Docker、Nginx 和网络就绪后自动检查:
- 当前不可变发布、app/caddy 健康状态和 40 位 release SHA;
- Nginx 对 Docker 的启动依赖与失败重试;
18000/18080私网监听边界;- Caddy → 宝塔 Nginx → 应用内部代理链;
- 公网
/api/health与宝塔 access log、site_total增量; - 宝塔原生十分钟任务、启用状态和日志路径;
- 登录会话、实验历史和实验 SSE。
脱敏 JSON 证据写入 /opt/eventshock/shared/logs/restart-verification-*.json。
任何检查失败或未提供认证 Cookie 时,状态只能是 FAIL 或 INCOMPLETE,待验证状态
会保留以便修复后重新执行 verify;只有全部检查为 PASS 才会清除待验证状态。
实际重启及其证据仍属于维护窗口外部门禁,脚本存在不能替代一次真实执行。
如需停止服务但保留数据:
sudo /opt/eventshock/current/scripts/compose-current.sh down再次强调,不要添加 --volumes。
目标 SHA 尚未出现全部三项检查或检查仍在运行。打开 GitHub 对应提交查看 Actions;不要在服务器上绕过检查。下一轮 10 分钟任务会重新查询。
至少一项必需检查不是 success。在同一功能分支修复、重新测试、commit 并 push;服务器只会考虑新的全绿 SHA。
部署分支发生了 force-push、rebase 或切换到了不包含当前发布 SHA 的历史。同步脚本按设计拒绝。停止自动任务并核对分支历史;不要直接删除状态文件规避保护。
依次确认 DNS、阿里云安全组 TCP 80/443、UFW、80/443 监听者和 Caddy 日志:
dig +short eventshock.mikezhuang.cn A @1.1.1.1
sudo ss -ltnp '( sport = :80 or sport = :443 )'
sudo /opt/eventshock/current/scripts/compose-current.sh logs --tail=100 caddy先通过宝塔原生任务触发同一条自动自愈链路并查看日志:
sudo /opt/eventshock/bin/register-baota-task.py --run
sudo grep -E 'RUNTIME_(DRIFT|SELF_HEAL_)|TARGET_SELF_HEAL|INFRASTRUCTURE_BLOCKED' \
/opt/eventshock/shared/logs/github-sync.log | tail -n 50
sudo systemctl status nginx.service --no-pager
sudo systemctl cat nginx.service注册器会重新识别当前 Caddy Docker bridge/subnet、迁移项目自有的旧窄 UFW 规则,并从 Caddy 容器内验证 Nginx 健康接口。Docker network 被重建后不能沿用旧网络规则;若日志显示未知或宽范围 18080 规则,必须先人工核对,注册器不会擅自删除。
若主机刚重启且 80/443 尚未监听,先查看 Caddy 门控日志。waiting for application 表示 FastAPI 尚未就绪,waiting for proxy 表示宝塔 Nginx 或其到应用的链路尚未就绪;依次出现两条 is ready 后,Caddy 才会启动。若出现 startup gate timed out,说明链路在默认 90 秒内仍未恢复,Caddy 已为保留 TLS 与错误可观测性而启动,必须继续检查 Nginx、UFW 与应用日志:
sudo /opt/eventshock/current/scripts/compose-current.sh logs --tail=100 caddy
sudo /opt/eventshock/current/scripts/compose-current.sh exec -T caddy \
wget -q -O- -T 3 http://127.0.0.1:2019/config/使用 sudo ufw status numbered 核对是否只有带 EventShock-Caddy-Nginx 注释的精确 18080 规则。不要用 sudo ufw allow 18080、面板“全部来源”放行或云安全组规则来临时解决超时;这些做法会把内部代理端口暴露到公网。只有在紧急诊断且明确接受暂时绕过宝塔流量统计时,才可短期把 CADDY_UPSTREAM 改为 app:8000;诊断结束后必须恢复正式值并重新完成内部、公网与流量统计验证。
先用 free -h 和 docker system df 检查资源,不要删除持久卷。前端已经在本地与 CI 构建并提交,服务器构建只复制 frontend/dist,不会运行 Node;仍然内存不足时,应先保留失败日志并评估交换空间或主机资源。