CaiWuXiTong/docs/开发与部署SOP.md

12 KiB
Raw Blame History

CaiWuXiTong 开发与部署 SOP

1. 这套流程解决什么问题

本项目由 6 位新人使用 Codex 协作开发。项目负责人只确认方向、验收结果和高风险动作,不负责日常命令操作。

固定链路如下:

任务分支开发 -> 本机验证 -> PR -> 合并 main
    -> 开发机复用缓存构建镜像
    -> 经 SSH 隧道只上传变化的镜像层
    -> 2GB 服务器只拉取本机 Registry 并切换镜像
    -> 本机后端检查 + 公网 HTTPS 验收
    -> 保留当前与上一个镜像,清理其余本项目产物

服务器永远不构建源码。

2. 每台开发机只做一次的设置

  1. 安装并启动 Docker Desktop,使用 WSL2 后端。
  2. 在 Docker Desktop 的 Settings -> Resources -> Advanced 中:
    • Docker 虚拟磁盘上限建议 60GB;
    • Windows 磁盘至少保留 15GB 可用空间。
  3. SSH 私钥放到个人目录,例如 C:\Users\你的用户名\.ssh\caiwu.pem,不要放进 Git。
  4. 只使用仓库里的发布脚本。不要自行执行 docker system prune -a,也不要重置 Docker Desktop。

说明:Docker 虚拟磁盘清理后,Windows 上看到的 .vhdx 文件可能不会立刻变小,但内部可用空间已经释放。不要为了让文件数字变小而删除虚拟磁盘。

3. 为什么后续不会反复全量下载和上传

项目使用专属构建器 caiwu-builder,缓存与其他项目分开:

  • 前端在 Windows 主机使用项目专用 Node 20 构建,避开 Docker 虚拟机内存上限;
  • JavaScript、Tailwind CSS 使用独立输入指纹;页面模板变化只重建 CSS,纯后端改动不重复打包前端;
  • PHP 扩展层只在 Dockerfile 的基础环境变化时重建;
  • Composer 只在 composer.lock 变化时重新解析依赖,并复用下载缓存;
  • npm 优先使用本机下载缓存;package-lock.json 和 Node 版本未变化时直接复用 node_modules;
  • JavaScript 公共依赖抽取为共享包,Node 堆上限为 4GB,压缩线程默认限制为 2;
  • 业务代码变化只重新执行镜像中的代码复制;
  • 日常命令不带 --pull 和 --no-cache。
  • 首次初始化才上传应用和 MariaDB 的全量包;日常发布通过 SSH 隧道连接服务器本机 Registry。
  • Registry 按内容哈希判断镜像层是否已经存在,基础环境、PHP 依赖和未变化的前端层不会重复上传。
  • 日常传输使用 BuildKit 原生 OCI 分层包,保持未变化层的压缩摘要稳定;docker save 全量包只用于首次初始化。
  • Registry 只监听服务器 127.0.0.1:5000,公网无法直接访问;发布脚本退出时会关闭 SSH 隧道。
  • 每次发布后只保留当前和上一个 Registry 版本,并回收旧版本不再引用的镜像层。

首次构建和首次上传必然较慢;后续构建应大量显示 CACHED,上传量由本次实际变化的镜像层决定。

4. 首次初始化服务器

首次发布由 Codex 或当班开发者在仓库根目录执行:

.\scripts\deploy-from-workstation.ps1 `
  -Server 118.196.84.79 `
  -KeyPath "C:\Users\你的用户名\.ssh\caiwu.pem" `
  -BootstrapServer

初始化脚本会:

  • 使用服务器现有 Ubuntu 安全更新源安装并启动 Docker 与 Compose;
  • 为 2GB 服务器增加 2GB swap(交换空间),仅用于运行时缓冲;
  • 创建 /opt/caiwuxitong/runtime/.env;
  • 在服务器本地生成数据库密码和 Laravel APP_KEY,不会打印出来;
  • 创建持久化数据库卷和上传文件卷。
  • 上传应用与 MariaDB 首次全量包,并初始化仅限本机访问的增量 Registry。

云平台安全组必须放行 TCP 80 和 TCP 443。应用容器只绑定服务器本机 127.0.0.1:18088,公网由轻量 Nginx 提供 HTTPS;Registry 的 5000 端口仍禁止对公网开放。

首次部署后执行一次 HTTPS 配置:

.\scripts\deploy-from-workstation.ps1 `
  -Server 118.196.84.79 `
  -KeyPath "C:\Users\你的用户名\.ssh\caiwu.pem" `
  -ConfigureHttps

该步骤在服务器安装轻量 Nginx 和独立 Certbot 环境,为公网 IP 申请 Let’s Encrypt 短期证书,并启用每 8 小时检查一次的自动续期。服务器仍不构建任何应用源码或镜像。

首次打开 https://118.196.84.79 后,由负责人或指定人员在网页安装向导中设置管理员账号和密码。

5. 日常正式发布

前提:PR 已审核并合并,当前为干净的 main。

git switch main
git pull --ff-only

.\scripts\deploy-from-workstation.ps1 `
  -Server 118.196.84.79 `
  -KeyPath "C:\Users\你的用户名\.ssh\caiwu.pem"

脚本自动完成:

  1. 检查分支和未提交改动;
  2. 使用项目专用 Node 20 和本机 npm 缓存构建前端资源;
  3. 使用提交 SHA 构建镜像并复用项目专属缓存;
  4. 将应用镜像导出到临时文件,通过 SSH 隧道只上传 Registry 缺少的镜像层;
  5. 服务器从本机 Registry 拉取新版本并启动;
  6. 检查服务器本机后端页面;
  7. 失败时恢复上一个镜像;
  8. 使用服务器互斥锁阻止两个人同时切换版本;
  9. 删除本地临时镜像包并关闭 SSH 隧道;
  10. 开发机、服务器 Docker 和服务器 Registry 均仅保留当前和上一个本项目版本;
  11. 清理 7 天前的本项目构建缓存,并将总量压到最多 3GB。

6. 数据库迁移

默认禁止自动迁移。脚本发现上次部署后新增了 database/migrations 文件时会停止。

只有负责人明确批准、PR 已写清影响与回滚思路后,才允许:

.\scripts\deploy-from-workstation.ps1 `
  -Server 118.196.84.79 `
  -KeyPath "C:\Users\你的用户名\.ssh\caiwu.pem" `
  -AllowMigrations

镜像回滚不能自动撤销已经执行的数据库结构变更,因此迁移授权必须单独处理。

7. 测试分支发布

正式服务器默认只允许 main。确需临时测试时,必须得到明确授权,并添加:

-AllowNonMain

测试完成后仍要回到 main 再正式发布,不能把测试分支当长期生产版本。

8. 上游财务数据库同步

生产环境默认设置 JIANSUN_ENABLED=false。上游按不可修改的只读旧系统处理,不要求对方修改账号、表结构、索引、字段或数据库功能;只有现有访问方式通过受控连通性和单查询探测后,才在服务器私有配置中开启。

启用后采用“活跃查询快照”策略:

  1. 页面优先读取持久化在 caiwu_storage 卷中的本地快照;相同数据范围和查询条件复用同一结果,与系统账户数量无关;
  2. 列表和汇总默认保持 2 分钟新鲜,下拉选项等低频数据缓存 60 分钟;
  3. 新鲜快照直接返回;过期快照也立即返回,同时登记为紧急后台刷新,页面不等待上游;只有本地从未存在该查询时,才允许受控同步获取一次;
  4. /etc/cron.d/caiwu-jiansun-snapshots 每分钟触发一次,只处理最近 6 小时实际使用且已经到期的查询;
  5. 相同查询使用独立单飞锁,不同查询最多占用 2 个上游槽位,因此大量缓存命中不会被全局串行;
  6. 每轮默认最多刷新 2 个活跃查询,每分钟默认最多产生 6 个实际上游查询组;列表优先于汇总,汇总优先于选项;
  7. 连接 5 秒未建立或单次查询超过 15 秒即失败;慢查询和失败查询进入 5 分钟冷却,连接失败同时触发全局熔断,不连续重试;
  8. 服务器和上游不通时继续使用已有本地快照;完全没有本地数据时明确返回暂不可用,不绕过保护直接连接。

相关 API 使用 X-Caiwu-Data-Status(fresh / stale)和 X-Caiwu-Data-Synced-At 响应头报告数据状态,验收时不能只看 HTTP 200。

服务器私有 runtime/.env 只使用下列变量名,真实值不得写入 Git 或本文档:

JIANSUN_ENABLED=false
JIANSUN_ALLOWED_SCOPES=
DB_JIANSUN_HOST=
DB_JIANSUN_PORT=1433
DB_JIANSUN_DATABASE=
DB_JIANSUN_USERNAME=
DB_JIANSUN_PASSWORD=
DB_JIANSUN_ODBC_DRIVER=FreeTDS
JIANSUN_SNAPSHOT_TTL=120
JIANSUN_SUMMARY_TTL=120
JIANSUN_OPTIONS_TTL=3600
JIANSUN_ACTIVE_QUERY_TTL=21600
JIANSUN_ACTIVE_QUERY_LIMIT=300
JIANSUN_REFRESH_LIMIT=2
JIANSUN_MISS_BUDGET=6
JIANSUN_BUDGET_WINDOW=60
JIANSUN_MAX_CONCURRENCY=2
JIANSUN_FAILURE_COOLDOWN=300
JIANSUN_QUERY_FAILURE_COOLDOWN=300
JIANSUN_SLOW_QUERY_MS=5000
JIANSUN_SLOW_QUERY_COOLDOWN=300

JIANSUN_ALLOWED_SCOPES 支持逗号分隔的精确范围和 prefix.*。首次上线先开放完成探测的小表范围,例如 invoice-deductions.*;约 280 万行核心表对应范围必须单独验证,未验证前不得填写 *。

这套策略提供的是近实时查询快照,不等同于复制上游全库。对于没有可靠更新时间或删除标记的旧表,不能声称实现了完整行级增量;禁止为了追求秒级实时而周期性扫描数百万行。页面实时性依靠本地快照和一分钟后台刷新,历史一致性只能通过低频、小批量、可随时停止的核对补齐。

8.1 生产服务器不能直连上游时

使用一台指定开发机作为主中继,另一台作为备用中继。它们只转发 TCP 流量,不保存数据库凭据,也不在开发机执行同步查询。数据库凭据仍只存在于服务器私有 runtime/.env,应用仍通过同一套快照、限流和熔断逻辑读取数据。

安全边界:

  • 服务器创建专用低权限 SSH 用户,只允许远端端口转发到预先指定的 Docker 私有网桥地址和端口;禁止 Shell、TTY、Agent、本地转发和任意监听地址;
  • 监听地址只能是 Docker 私有网桥,不能是 0.0.0.0、公网网卡或宿主机公网端口;
  • 每台中继开发机生成独立 SSH 私钥,只把公钥交给具备生产权限的 Codex 或运维配置,禁止复制或共享私钥;
  • 最多配置一主一备。同一远端端口只会有一台成功监听,备用机每 30 秒左右低频重试;主机离线后备用机自动接管;
  • 隧道断开不触发页面连续查询:已有快照继续返回 stale,没有快照时明确暂不可用。

由具备生产授权的 Codex 或运维在中继开发机执行下面的通用安装脚本,项目负责人不负责日常运行这些命令。尖括号内容从私有配置取得,禁止写入 Git、PR 或群聊:

.\scripts\install-upstream-tunnel-task.ps1 `
    -ServerHost '<生产服务器>' `
    -PrivateKeyPath '<本机专用隧道私钥>' `
    -RemoteBindAddress '<服务器 Docker 私有网桥地址>' `
    -RemoteBindPort '<服务器私有监听端口>' `
    -UpstreamHost '<上游数据库地址>'

安装脚本创建当前用户的登录计划任务,实际守护逻辑位于 scripts/keep-upstream-tunnel.ps1。守护进程使用 SSH keepalive 检测断线并带随机延迟重连,避免多台备用机同时高频连接服务器。

启用前按以下顺序验收:容器到隧道端口 TCP 可达;只开放一个已探测的小表范围;第一次查询实际访问上游且在超时内完成;第二次相同查询命中本地快照且不再访问上游;最后才把服务器私有 JIANSUN_ENABLED 改为 true。核心大表不随总开关自动开放。

9. 禁止事项

  • 禁止登录服务器运行 docker build 或 docker compose build;
  • 禁止在服务器运行 Composer、npm 或前端打包;
  • 禁止把 .pem、.env、密码、数据库文件或 image.tar 提交到 Git;
  • 禁止把服务器 Registry 的 5000 端口开放到公网;
  • 禁止为了省空间删除 Docker 卷;
  • 禁止全局清空 Docker,因为同一台电脑可能还有其他项目;
  • 禁止跳过健康检查后口头说“部署完成”。

10. 发布完成时应报告

  • 发布分支和完整 Git SHA;
  • 镜像标签;
  • 服务器 caiwu_app、caiwu_db 状态;
  • https://118.196.84.79 的实际返回结果和证书有效期;
  • 是否执行数据库迁移;
  • 本地临时包是否删除;
  • 本地/服务器保留了几个本项目镜像;
  • 本次增量新增镜像层大小和传输耗时;
  • BuildKit 缓存当前占用。