Fizzy SaaS 模式部署指南:从 `saas:enable` 到 Kamal 多环境发布与 hotcell 附件处理单元 Fizzy SaaS 模式部署指南从saas:enable到 Kamal 多环境发布与 hotcell 附件处理单元【免费下载链接】fizzyKanban as it should be. Not as it has been.项目地址: https://gitcode.com/GitHub_Trending/fizzy2/fizzy导读Fizzy 是一个看板式项目管理与问题追踪工具卡片在列之间移动支持评论、提及与指派而saas/AGENTS.md是面向 37signals 托管版SaaS 模式的部署与开发指引回答了如何在本仓库中开启 SaaS 模式、如何用 Kamal 发布到 production / staging / beta / beta1 等多个目标环境、以及附件处理单元 hotcell 如何在部署中自动保持同步这三个核心问题。读完本文你将掌握Fizzy.saas?的判定机制、saas:enable对 bundle 与数据库适配器的实际影响、bin/kamal deploy -d destination的完整发布流程以及 hotcell 镜像内容哈希 pin 双钩子的同步原理。一、SaaS 模式开关文件、环境变量与判定优先级1.1 开关的三个事实来源SaaS 模式并非通过配置中心或数据库切换而是由一个 checkout 级文件 一个环境变量共同决定判定逻辑集中在 lib/fizzy.rbdef saas? return saas if defined?(saas) saas !!(((ENV[SAAS] || File.exist?(File.expand_path(../tmp/saas.txt, __dir__))) ENV[SAAS] ! false)) end从源码可以得出三条精确规则tmp/saas.txt存在即开启这是 checkout 级别的开关由两个 Rails 任务维护见 lib/tasks/saas.rakebin/rails saas:enable创建tmp/saas.txtbin/rails saas:disable删除它。SAAS环境变量优先级更高只要SAAS被设置为除false以外的任何值包括空串、0、off等都会强制开启而SAASfalse即使tmp/saas.txt存在也会强制关闭因为 ENV[SAAS] ! false短路失败。saas带记忆缓存首次调用后结果被缓存后续调用不再重读文件与环境变量因此运行时切换开关需要重启进程才生效。按 saas/AGENTS.md 的说明环境变量这条路径是给生产镜像与bin/ci用的而不是用来切换本地 checkout 的本地切换应始终使用saas:enable/saas:disable。1.2 开启后改变了什么开启 SaaS 模式并非加了一个功能开关这么简单它会改变每条bin/rails与bin/kamal命令的行为bundle 被替换Fizzy.configure_bundle在 SaaS 模式下将BUNDLE_GEMFILE指向Gemfile.saaslib/fizzy.rb。Gemfile.saas通过eval_gemfile Gemfile继承基础依赖再叠加 SaaS 专属组件fizzy-saas本 gempath: saas、queenbee、hotcell-client与activestorage-hotcell-client、原生推送action_push_native、遥测全家桶yabeda-*/sentry-*、审计console1984/audits1984等见 Gemfile.saas。默认数据库适配器变为 MySQLFizzy.db_adapter在 SaaS 模式下默认mysql否则默认sqlitelib/fizzy.rb。这与根 AGENTS.md 中搜索在 MySQL 上按账号 CRC32 分 16 片、在 SQLite 上是单一 FTS5 索引的描述互相印证——不同的 adapter 意味着完全不同的存储行为。因此先saas:enable再执行任何命令是保证命令作用于托管版而不是自托管版的前提。1.3 与其他文档的衔接关系根 AGENTS.md 是默认分支上的总纲默认分支为main其中明确指出tmp/saas.txt是bin/setup使用的 checkout 级开关当它存在时必须先读saas/AGENTS.md否则不得应用其指令。也就是说SaaS 模式文档只在其模式开启后生效避免与自托管部署config/deploy.yml docs/kamal-deployment.md互相污染。二、部署目标环境与配置文件的定位规则2.1 四个目标环境saas/AGENTS.md 明确列出发布目的地production、staging、beta、beta1。每个目标各有一份saas/config/deploy.destination.yml目标配置文件用途来自 saas/README.mdproductionsaas/config/deploy.production.yml生产环境https://app.fizzy.do使用 FlashBlade 桶做 blob 存储stagingsaas/config/deploy.staging.yml测试基础设施变更使用生产相似但独立的数据库与 Active Storagebeta模板saas/config/deploy.beta.yml测试产品功能复用生产数据库需要BETA_NUMBER环境变量beta1saas/config/deploy.beta1.yml当前唯一实际存在的编号 beta 目标https://beta1.fizzy-beta.com2.2-d destination如何选中正确配置关键机制在 saas/AGENTS.md 中强调过只有启用 SaaS 模式后bin/kamal才会通过-c参数指向本 gem 的saas/config/deploy.yml。跳过saas:enable直接部署Kamal 会读取根目录的config/deploy.yml——那是自托管示例对 37signals 的目标主机一无所知属于典型的命令成功但部署对象错误陷阱。saas/config/deploy.yml是总入口它通过 ERB 解析出 gem 目录、hooks 与 secrets 路径saas/config/deploy.ymlservice: fizzy image: basecamp/fizzy asset_path: /rails/public/assets hooks_path: % File.join(Gem::Specification.find_by_name(fizzy-saas).gem_dir, .kamal, hooks) % secrets_path: % File.join(Gem::Specification.find_by_name(fizzy-saas).gem_dir, .kamal/secrets) %2.3 beta 模板与 beta1 的实现细节beta不是一个独立的文件配置而是一份ERB 模板saas/config/deploy.beta.yml 开头即强制要求BETA_NUMBER环境变量缺失时直接raise并通过一张data哈希维护每个编号的 web/jobs/lb 主机与 solidqueue 数据库。而 saas/config/deploy.beta1.yml 只有三行把ENV[BETA_NUMBER]设为1后渲染共享的 beta 模板从而生成APP_FQDNbeta1.fizzy-beta.com与CACHE_NAMESPACE1等环境。值得注意的遗留物saas/.kamal/secrets.beta2到secrets.beta4是符号链接残留并不会让这些编号成为真实目标——从配置数据看beta 模板只定义了编号1。2.4 各目标环境的核心差异对比三份目的地配置可以发现托管版部署的共性骨架web 与 jobs 两类角色、Kamal 内置 proxy、ssh: user: app、大量的 secrets 注入RAILS_MASTER_KEY、MySQL 三套账号密码、VAPID、Stripe、APNs/FCM、Active Storage、Sentry、Active Record 加密密钥等。差异点同样值得注意production多机房多主机sc_chi/df_iad/df_ams/sjcretain_containers: 2数据库走 MySQL 主从MYSQL_DATABASE_HOST/REPLICA_HOST每个数据中心各有 solid cache 主机并额外声明独立的load-balanceraccessorykamal-proxyNFS 挂载证书且带otel_*标签接入遥测saas/config/deploy.production.yml。staging结构与生产几乎一致但数据库/证书路径全部换成fizzy-staging-*saas/config/deploy.staging.yml。beta单 web 单 jobs 主机且 solid queue 与 solid cache 是独立小数据库fizzy-beta-solidqueue-db-101、fizzy-beta-solidcache-db-101数据库本身MYSQL_DATABASE_HOST仍指向生产的fizzy-mysql-primary呼应了 README 中beta 复用生产数据库的说明。三、发布流程一条命令部署两个容器3.1 标准发布命令bin/rails saas:enable # 部署前置步骤必须先执行 bin/kamal deploy -d destinationsaas:enable是部署的前置条件而不仅是本地开发的开关。发布时pre-build与pre-deploy两个钩子会负责发布 hotcell cell 镜像并按需重启 accessory因此一次deploy同时交付两个容器应用本体 负责附件处理的 hotcell cell。3.2 钩子机制镜像先发布、容器后重启两个钩子位于 saas/.kamal/hookspre-buildsaas/.kamal/hooks/pre-build——在部署锁之前、不做任何 ssh的阶段执行if [ -z ${SKIP_HOTCELL_CHECKS:-} ]; then $repo_root/saas/hotcell/bin/check ${KAMAL_DESTINATION:-production} --version$KAMAL_VERSION --publish fipre-deploysaas/.kamal/hooks/pre-deploy——在部署锁之内、应用启动之前执行if [ -z ${SKIP_HOTCELL_CHECKS:-} ]; then $repo_root/saas/hotcell/bin/check ${KAMAL_DESTINATION:-production} --version$KAMAL_VERSION \ ${KAMAL_HOSTS:--hosts$KAMAL_HOSTS} --reboot fi两个钩子都从被部署的提交KAMAL_VERSION读取 pin因此bin/kamal rollback也会把 cell 一起回滚它们都尊重--hosts/--roles对两个主机的部署只会重启这两个主机上的 cell。SKIP_HOTCELL_CHECKS1可跳过两者——仅用于明知 cell 有问题也要强行部署的场合。3.3 手动检查与修复saas/hotcell/bin/check destination可随时手动报告 cell 状态及修复方案--apply一步修复。其退出码直接命名修复动作见 saas/hotcell/bin/check2pin 的镜像需要构建并推送registry 缺失3只需重启 accessory镜像已在但主机未运行它1检查本身无法判定docker 未登录、ssh 不通等此时宁可失败也不猜测。脚本还支持--versionSHA从指定提交而非工作树读 pin这是 rollback 语义的基石、--hostsa,b只询问并重启这些主机、-q只输出结论供调用方按退出码行动。核心检查逻辑是两步分离先本地docker manifest inspect判断 pin 是否已发布隔离没人构建与没人重启两种故障再通过 ssh 用docker inspect对比每台主机实际运行的镜像。3.4 环境变量与 secrets 注入部署环境变量分两类注入以 production 为例env.clear是明文值RAILS_ENV、MySQL 主机、RAILS_LOG_LEVEL: fatal以抑制非结构化日志等env.secret是从 secrets 文件读取的清单Stripe、VAPID、APNs/FCM、加密密钥等 30 余项env.tags按数据中心打标签如PRIMARY_DATACENTER: true只出现在 df_iad。secrets 文件位于 saas/.kamalsecrets.production、secrets.staging、secrets.beta1等部署时依赖 1Password CLI 提供凭据。四、hotcell无网络、无特权的附件处理单元4.1 为什么需要它图片变体、blob 分析与 PDF/视频预览这类处理不再运行在持有数据库凭据的应用进程内而是放到一个无网络、持有极少东西的兄弟容器里——这是攻击面收窄的设计。cell 的代码在saas/hotcell/Dockerfile、独立Gemfile、config.rb中的资源上限以及operations/下的具体操作active_storage、echo、reopen。4.2 镜像 pin内容是哈希不是 git 提交saas/hotcell/bin/image 定义了镜像仓库名与 tag 算法digest$(cat $cell_dir/Dockerfile $cell_dir/Gemfile $cell_dir/Gemfile.lock $cell_dir/config.rb \ $cell_dir/operations/*.rb | sha256sum | cut -c1-12) echo $HOTCELL_IMAGE_REPOSITORY:$digest即 tag 对Dockerfile、Gemfile、Gemfile.lock、config.rb、operations/*.rb拼接内容的 SHA-256 前 12 位。选择内容哈希而非 git 提交的原因在脚本注释中交代得很清楚提交派生的 tag 会随目录内任何变更包括构建脚本和 pin 本身漂移导致bump gem 并 pin 结果的那个提交永远无法命名自己。相同字节得到相同 tag改动镜像不包含的内容则 tag 不变。与之配套的三条铁律tag 不可变、没有latestkamal accessory reboot会拉取该时刻 tag 指向的内容可移动 tag 会让主机运行内容取决于最近一次重启时间build会锁定 cell 的 Gemfile.lock 到应用侧的 hotcell 版本客户端与服务端差一个版本就会在每次请求上报protocol错误因此应用 lockfile 是事实源头先动应用 gem再 buildcheck --publish拒绝推送未提交的 pinbuild 是唯一手工步骤因为它写出的 pin 属于提交本身。4.3 资源上限与安全基线saas/hotcell/config.rb 设置的是上限而非默认具体操作自己的限制会被钳制到此HotCell.limits concurrency: 4, queue_size: 8, queue_wait: 10, deadline: 120, # 秒对齐视频预览器的 120s 截止 memory: 1536 * 1024**2, # 字节对齐镜像转换器的 256MB file_size 上限 file_size: 256 * 1024**2accessory 声明在 saas/config/deploy.ymlnetwork: none必须放在 accessory 级而非options:否则会与 Kamal 自带的--network kamal冲突成两个标志导致 Docker 拒绝容器、cpus: 2、memory: 2g且memory-swap与内存相等缺省时 Docker 允许两倍 swap 会让上限失效、read-only: true、cap-drop: ALL、no-new-privileges、user: 10001:10001、pids-limit: 512。scratch 磁盘放在宿主的回环文件系统4G、nosuid/nodev/noexec而非 tmpfs填满时以ENOSPC呈现为瞬时错误不会记入 blob。Dockerfile 进一步夯实安全与稳定性分两阶段构建编译器只存在于 build 阶段运行镜像不携带运行镜像只装libvips42、mupdf-tools、ffmpeg明确不装 LibreOffice——Fizzy 接受 office 文档但从不预览其解析器属于买来的无效爆炸半径uid/gid 10001 无 home 无 shell启动前find / -xdev -type f -perm /06000 -exec chmod a-s清除所有 setuid 位。镜像内还针对 OpenMP 线程数与容器 CFS 配额不一致导致的 worker 死亡问题显式设置OMP_NUM_THREADS2、OMP_THREAD_LIMIT8。4.4 应用与 cell 的跨容器协作saas/config/deploy.yml中HOTCELL_ROOT/run/hotcell注册 cell注册后所有转换都交给它未设置则一切在应用内跑HOTCELL_GROUP10001是双方共享的 gid。这里有一个精妙的文件传递设计应用角色使用group-add: 10001打开文件描述符cell 通过按名重新打开reopen来消费。因为工具拿到文件名后会以/dev/fd/N重新打开调用者的描述符——这是一次新的 open会按 cell 的 uid 重新校验权限0600 的临时文件必然 EACCES。必须让应用属组包含 cell 的 gid否则故障只会从 cell 内的 EACCES 移到应用内的 EPERM。/hotcellz只询问控制 socketdescribe、metricssupervisor 内联回答不 fork并返回OK/FAIL200/503未认证但只暴露两个探针指标/hotcellz/test仅 staff 可访问返回完整 JSON 诊断包括两条走工作 socket真正传文件的 socket的往返example.echo直接读描述符example.reopen按名重开——组配置错误的 cell 会 echo 完美而 reopen 失败因此监控探针永远看不到工作 socket 的故障配置变更后应手工运行/hotcellz/test或Cell.diagnostics(work: true)。4.5 变更 hotcell 的完整流程saas/README.md 给出的黄金流程CI 会通过 saas/test/lib/hotcell_accessory_test.rb 检查 pin 是否仍与当前树构建一致saas/hotcell/bin/build --platformlinux/amd64 # 1. 构建并 pin saas/config/deploy.yml bin/rails test saas/test/lib/hotcell_accessory_test.rb # 2. 跑测试 # 3. 一起提交两个 lockfile、pin、saas/hotcell/ 下的变更 bin/kamal deploy -d destination # 4. 部署钩子自动发布镜像并重启 cell # 5. 验证/hotcellz 返回 OK/hotcellz/test 全过Prometheus 每台主机 hotcell_up 1Loki 无 WARN/ERROR本地开发时bin/dev会在应用旁通过saas/Procfile.dev以 foreman 启动非容器化 cell容器在 macOS 上无法接收文件描述符bin/dev --push额外加载 APNs/FCM 生产凭据用于原生推送测试。五、代理、SSH 别名与日常运维saas/config/deploy.yml末尾还定义了三条 Kamal alias对应 37signals 日常使用的三条命令aliases: console: app exec -i --reuse -e CONSOLE_USER:% ENV[USER] % bin/rails console query: app exec -q --reuse -p -e CONSOLE_USER:% ENV[USER] % CLAUDECODE:% ENV[CLAUDECODE] % CODEX_THREAD_ID:% ENV[CODEX_THREAD_ID] % bin/rails query ssh: app exec -i --reuse -e CONSOLE_USER:% ENV[USER] % /bin/bash维护模式通过负载均衡器上的 kamal-proxy 启停详见 saas/README.mdknife ssh hostname:fizzy-lb-* sudo docker exec fizzy-load-balancer kamal-proxy stop fizzy --message...下线kamal-proxy resume fizzy恢复。六、变更 SaaS 依赖后如何同步回 Fizzy对fizzy-saasgem 本身做改动后需要在 Fizzy 侧重新解析依赖BUNDLE_GEMFILEGemfile.saas bundle update --conservative fizzy-saas本地开发 Stripe 集成时saas/README.mdeval $(BUNDLE_GEMFILEGemfile.saas bundle exec stripe-dev) bin/dev # 必须在同一终端会话启动开发服务器总结saas/AGENTS.md虽短却勾勒出 37signals 托管 Fizzy 的完整部署骨架文件与环境变量双路判定的 SaaS 开关lib/fizzy.rb、saas:enable前置的 Kamal 发布流程、内容哈希 pin pre-build/pre-deploy 双钩子驱动的双容器同步saas/.kamal/hooks以及无网络、无特权的 hotcell 附件处理单元saas/hotcell。理解这些机制后无论是本地切换模式、发布到四个目标环境还是安全地变更 hotcell都能按这套明确的分工行事saas:enable→saas/hotcell/bin/build仅当 cell 内容变更→bin/kamal deploy -d destination→ 用/hotcellz与/hotcellz/test验证。【免费下载链接】fizzyKanban as it should be. Not as it has been.项目地址: https://gitcode.com/GitHub_Trending/fizzy2/fizzy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考