Fizzy SaaS 模式全解析:fizzy-saas 引擎、Hotcell 附件隔离处理与 Kamal 部署指南
【免费下载链接】fizzyKanban as it should be. Not as it has been.项目地址: https://gitcode.com/GitHub_Trending/fizzy2/fizzy
本指南以 saas/README.md 为骨架,结合仓库内引擎源码、构建脚本、部署配置与测试用例,系统讲解如何在开源 Fizzy(看板应用)上开启 SaaS 模式、接入 Stripe 计费与原生推送通知,以及核心亮点——通过 Hotcell 在无网络、无特权的旁路容器(cell)中隔离处理图片变体、文件分析与 PDF/视频预览的完整架构与部署链路。读完你将掌握 SaaS 模式的开关机制、本地开发与部署流程,以及一套可复制的"内容哈希镜像 + 自动发布/重启钩子"的旁路组件治理方案。
一、fizzy-saas 是什么
fizzy-saas 是一个 Rails Engine,由 37signals 随 Fizzy 一起打包发布,用于承载托管版本(https://fizzy.do)。它不是一个独立的应用程序,而是叠加在开源 Fizzy 之上的 SaaS 增强层。从 fizzy-saas.gemspec 可以看到它的定位:
- 名称与版本:
fizzy-saas,版本号由 lib/fizzy/saas/version.rb 提供; - 授权:以 O'Saasy License 发布(见 saas/LICENSE.md),与开源 Fizzy 的许可相互独立;
- 职责范围:gem 打包的内容集中在
app、config、db、lib、exe目录,可执行文件为push-dev与stripe-dev两个开发辅助脚本; - 依赖:除 Rails 外,还依赖
queenbee(订阅/账户体系)、console1984/audits1984(受保护的数据库控制台与审计)、yabeda全家桶(Prometheus 指标)、sentry-ruby(错误上报)等。
引擎的装载入口是 lib/fizzy/saas/engine.rb,它通过一系列initializer完成如下挂载:
- 将自身路由挂载到
/(mount Fizzy::Saas::Engine => "/",路由定义见 saas/config/routes.rb),补充my/devices、admin/stats、admin/console、hotcellz与hotcellz/test等端点; - 让原生推送设备(ActionPushNative)与 SaaS 相关模型走独立的
saas数据库(SaasRecord通过connects_to database: { writing: :saas, reading: :saas }连接,见 saas/app/models/saas_record.rb); - 插入
TrackTrueClientIp中间件(在 Rails 的RemoteIp之前把 Cloudflare 的True-Client-IP拷贝进X-Forwarded-For,见 saas/lib/fizzy/saas/true_client_ip.rb); - 在
active_storage.configs之前注册 Hotcell cell(Cell.register!)并合并 Active Storage 的远端处理配置; - 在
to_prepare阶段把计费、存储限额、原生推送目标等模块混入主应用的Account、Identity、Signup、CardsController等类。
从源码结构看,SaaS 化还体现在计费与限额上:订阅模型 saas/app/models/subscription.rb 定义了FreeV1免费档位;存储限额模型 saas/app/models/account/storage_limited.rb 设置了默认 1GB 上限与 500MB 预警阈值,并提供exceeding_storage_limit?/nearing_storage_limit?/add_storage_exception等判定与豁免接口。
二、在开源 Fizzy 上切换 SaaS 模式
SaaS 模式与开源模式的切换由两个 Rails 任务完成:
bin/rails saas:enable # 开启 SaaS 模式 bin/rails saas:disable # 回到开源模式结合 saas/AGENTS.md 的说明可以理解其底层机制:
saas:enable创建tmp/saas.txt标记文件,saas:disable删除它;Fizzy.saas?同时读取环境变量SAAS——任何非false的值都会启用,而SAAS=false即使文件存在也会强制关闭(该环境变量路径用于生产镜像与bin/ci,不适合用来手动切换本地检出);- 启用后会同时完成两件事:把 bundle 切换到
Gemfile.saas(引擎依赖、推送、队列等完整依赖),并把默认数据库适配器切换为 MySQL(对应 config/database.mysql.yml)。也就是说,saas:enable会改变后续所有bin/rails与bin/kamal命令的默认行为; - 部署前置条件:只有处于 SaaS 标志之下,
bin/kamal才会传入-c指向本引擎的 saas/config/deploy.yml;否则 Kamal 会读取根目录的 config/deploy.yml——那是自托管示例配置,对托管环境的部署目标一无所知。
引擎自带测试任务,运行全部 SaaS 测试使用:
bin/rails test:saas该任务定义在 saas/lib/tasks/fizzy/saas_tasks.rake,收集引擎test/**/*_test.rb下所有用例。
三、如何把 gem 改动同步回 Fizzy
fizzy-saas 以 gem 形式存在,修改完引擎代码后,需要让主应用依赖更新才能生效:
BUNDLE_GEMFILE=Gemfile.saas bundle update --conservative fizzy-saas这里显式指定Gemfile.saas作为 bundle 入口,--conservative只升级fizzy-saas本身而不顺带升级其他依赖,避免 SaaS 环境发生意外的间接版本漂移。这也是引擎开发的标准迭代节奏:改 gem → 更新主应用锁文件 → 运行测试。
四、本地对接 Stripe 计费
首次使用 Stripe 集成需要两步准备:
- 安装 Stripe CLI;
- 执行
stripe login并授权37signals Development环境。
此后,在本地开发 Stripe 集成时,需要通过脚本建立隧道并注入环境变量:
eval "$(BUNDLE_GEMFILE=Gemfile.saas bundle exec stripe-dev)" bin/dev # 必须在同一个终端会话里启动开发服务器eval是关键——stripe-dev(声明于 fizzy-saas.gemspec 的 executables)向标准输出打印环境变量赋值语句,只有被当前 shelleval后,bin/dev启动的进程才能继承这些变量。脚本会请求 1Password 授权,以读取并设置 Stripe 所需的凭据。
Stripe 按环境划分为三套独立账户:
- Development:沙箱测试环境,配合 Stripe CLI 的本地隧道做开发验证;
- Staging:基础设施验证环境,使用独立的测试密钥;
- Production:真实计费环境,仅通过受控部署接触。
底层数据模型上,订阅表account_subscriptions保存stripe_customer_id(唯一索引)与stripe_subscription_id(见 saas/db/migrate/20251203144630_create_account_subscriptions.rb),引擎在to_prepare中通过Queenbee::Subscription.short_names = Subscription::SHORT_NAMES把计费档位动态注册为顶层常量(如FreeV1Subscription)。
五、本地测试原生推送通知(APNs / FCM)
要在本地验证原生推送(APNs 与 FCM),用--push标志启动开发服务器:
bin/dev --push这会请求 1Password 授权,拉取推送凭据并注入环境。需要注意:本地加载的是生产环境的 APNs 与 FCM 凭据。其实现位于 saas/exe/push-dev:脚本通过op read从 1Password 的Deploy/Fizzy/Production条目读取APNS_KEY_ID、APNS_ENCRYPTION_KEY_B64、FCM_ENCRYPTION_KEY_B64,并输出export ...语句与ENABLE_NATIVE_PUSH="true",因此同样需要eval "$(bundle exec push-dev)"式用法才能生效。
引擎侧,原生推送设备模型(ApplicationPushDevice)由 saas/app/models/application_push_device.rb 提供,并注册了名为:native的推送目标(Notification.register_push_target(:native));对应设备管理界面与接口位于my/devices(控制器见 saas/app/controllers/my/devices_controller.rb)。
六、Hotcell:把附件处理隔离到无特权旁路容器
这是本仓库 SaaS 层最值得深入的部分。图片变体(variant)、blob 分析以及 PDF / 视频预览,可以运行在一个无网络、无特权、与数据库凭据隔离的兄弟容器中,而不是在持有数据库凭据的应用进程里执行。这个容器被称作cell,全部相关代码位于 saas/hotcell/:其 Dockerfile、独立的 Gemfile 与 Gemfile.lock、资源上限配置 config.rb,以及它对外提供的操作(operations)。
6.1 为什么要一个"cell"
思路来自最小化爆炸半径(blast radius):处理用户上传的任意二进制内容(图片、PDF、视频)意味着要运行解析器和转换器,这些工具链一旦有漏洞,若跑在应用进程内就直接暴露数据库凭据。把这类工作放进一个只有两个 Unix socket、没有网络的兄弟容器,即使解析器被攻破,攻击者也拿不到任何网络能力,更拿不到数据库凭据。Dockerfile 中这段注释点明了设计哲学——"cell 的镜像内所有东西都在爆炸半径内,请把这个文件当作预算而不是清单"。
6.2 本地如何运行 cell
在 SaaS 模式下,bin/dev会通过 saas/Procfile.dev 与 foreman 在服务器旁启动一个 cell:
bin/dev # cell 随开发服务器一起启动,承担附件处理,与生产行为一致本地 cell不容器化运行,原因在 README 中写得很清楚:macOS 上 Docker 运行在一个 Linux VM 里,容器无法接收文件描述符——SCM_RIGHTS无法跨两个内核传递,所以开发环境让 cell 直接以宿主机进程方式跑。Procfile 中cell:一行的要点是:
cell: env -u RUBYOPT -u RUBYLIB ... BUNDLE_GEMFILE=$PWD/saas/hotcell/Gemfile \ HOTCELL_DIR=$PWD/tmp/hotcell/active_storage \ HOTCELL_CONFIG=$PWD/saas/hotcell/config.rb \ HOTCELL_OPERATIONS=$PWD/saas/hotcell/operations \ bundle exec hotcell --development它显式清掉主应用注入的 bundler 相关环境变量(-u),改用 cell 自己的Gemfile与自己的配置,并把操作目录指向 saas/hotcell/operations/。由于开发环境的 cell 直接在你的笔记本上执行命令,Dockerfile 里的每个工具(libvips42、mupdf-tools、ffmpeg)都需要宿主机等价物——bin/setup会安装它们;如果你往镜像里加了新工具,必须同步加进.mise.toml和Brewfile,否则该操作只会在开发环境失败。
6.3 /hotcellz:cell 的可达性检查
/hotcellz回答"cell 是否可达"这一简单问题,路由与控制器定义见 saas/config/routes.rb 与 saas/app/controllers/hotcellz_controller.rb。它有两个端点,成本完全不同:
| 端点 | 鉴权 | 检查内容 | 成本 |
|---|---|---|---|
/hotcellz | 匿名 | 仅控制 socket(describe与metrics) | 极低,supervisor 内联回答,不 fork |
/hotcellz/test | 仅 staff | 完整诊断,含两趟工作 socket 往返 | 每趟都 fork 一个 worker |
/hotcellz返回OK(200)或FAIL(503)。它是匿名端点,便于监控系统轮询,除此之外什么也不透露——"陌生人不该知道 cell 的库存或负载";/hotcellz/test以 JSON 返回完整诊断(at、host、root以及各检查项的ok/error),逻辑在 saas/lib/fizzy/saas/cell.rb 的Cell.diagnostics(work: true)中实现。staff 门槛由Current.identity&.staff?强制(见ensure_staff_access,未登录直接403,不跳登录页——探针要的是答案)。
为什么需要两趟"工作 socket"往返,并且为什么它们要藏在 staff 之后?因为两个示例操作证明的是同一个机制的两半:
example.echo(saas/hotcell/operations/echo.rb)直接读取调用者传来的文件描述符,证明SCM_RIGHTS端到端传递成功;example.reopen(saas/hotcell/operations/reopen.rb)按名字重新打开输入输出(Linux 上是/dev/fd/N,macOS 上是文件自身路径),这是一次全新的 open,会按 cell 的 uid 重新做权限检查——所有把文件名交给外部工具的操作走的都是这条路。
因此:一个 group 配错的 cell,echo完美通过而reopen失败——单看echo会把一个坏掉的 cell 误判为健康。加上每趟往返都要 fork worker,所以这两趟检查被放在鉴权之后。
结论是设计性的:监控看不到坏掉的工作 socket,只有这两趟往返能发现,而它们仅限 staff——因为那属于配置错误而不是会自我恶化的故障,所以每当配置变更时,应该手动执行/hotcellz/test或Cell.diagnostics(work: true)验证。
6.4 两个开关:HOTCELL_ROOT 与 HOTCELL_GROUP
cell 的启停完全由两个环境变量控制,README 中的开关表如下:
| 变量 | 作用 | 所在位置 |
|---|---|---|
HOTCELL_ROOT | 注册 cell,注册后所有转换都由 cell 承担;不设置则一切在应用内完成 | saas/config/deploy.yml |
HOTCELL_GROUP | 应用与 cell 共享的 gid,使 cell 能按名字打开应用交给它的文件;必须与应用的group-add及 cell 自身的 gid 一致;开发环境不设置(两侧以同一用户运行) | saas/config/deploy.yml |
源码层面(saas/lib/fizzy/saas/cell.rb):
Cell.root读取HOTCELL_ROOT,若在非 local 环境且未设置(且没有SECRET_KEY_BASE_DUMMY——那是 Dockerfile 里资产预编译时启动生产环境的场景),会直接抛HotCell::ConfigurationError,强制要求生产必须显式配置;Cell.group读取HOTCELL_GROUP,gem 的 setter 会做数值校验;Cell.enabled?即root.present?;register!用timeout: 135注册名为active_storage的 cell——135 秒必须覆盖 cell 的answer_within(即queue_wait + deadline + reply),否则一个饱和的 cell 会表现为传输层失败而非它自己的判决;同时把UnprocessableAttachment标记为永久性错误、ProcessingUnavailable标记为瞬态错误("继承关系就是分类");Cell.active_storage_configuration在启用时把 Vips 变体处理器、Image/Video/Audio 分析器、PDF/视频预览器全部换成 HotCell 客户端实现,并通过引擎 initializer(before: "active_storage.configs")合并进app.config.active_storage。
对应地,saas/config/deploy.yml 中env.clear设置:
HOTCELL_ROOT: /run/hotcell HOTCELL_GROUP: 10001为什么 gid 必须三方一致?因为按文件名重开描述符(reopen路径)是一次按 cell 的 uid 重新校验的全新 open,应用拥有的 0600 临时文件对 cell 是EACCES。解决方案是group-add: 10001加到 web 与 jobs 两个角色上(见 saas/config/deploy.yml 的servers.web.options与servers.jobs.options),让应用把文件放进共享组:缺了它,错误只是从 cell 里的EACCES挪到应用里的EPERM,并没有消失。三处数字(cell 的--user 10001:10001、角色的group-add 10001、HOTCELL_GROUP=10001)必须一致,而 saas/test/lib/hotcell_accessory_test.rb 中的测试 "the app shares the cell's group" 正是跨所有部署目标校验这一点。
6.5 cell 的资源上限与内部结构
saas/hotcell/config.rb 定义的是天花板而非默认值——单个操作自身的上限会被钳制到这些值以内,因此它们要按最苛刻的操作来定(视频预览器的 120 秒 deadline、图片转换器的 256MB 文件大小):
HotCell.limits concurrency: 4, queue_size: 8, queue_wait: 10, deadline: 120, memory: 1536 * 1024**2, file_size: 256 * 1024**2单位说明:deadline与queue_wait是秒,memory与file_size是字节(这里均为 1536MB / 256MB)。
Dockerfile 的工程细节同样值得留意:
- 分阶段构建:bundle 在
build阶段编译(需要编译器),而真正运行的镜像不携带编译器——"运行不可信字节的镜像不能带编译器"; - 安全补丁自持:
apt-get upgrade主动应用 Debian 的待发布安全补丁,避免等上游ruby:3.4-slim重建; - 工具集最小化:只装
libvips42、mupdf-tools、ffmpeg。没有 LibreOffice——Fizzy 接受 office 文档但从不预览,装它的解析器等于白花钱买爆炸半径; - 最小权限用户:
hotcell用户 uid/gid 10001,无 home、无 shell,HOME=/tmp;移除镜像内所有 setuid/setgid 位(chmod a-s),与应用侧no-new-privileges双重独立防护; - 线程池对齐:
OMP_NUM_THREADS=2匹配 cell 的 CPU 配额(cpus: 2是 CFS 配额而非亲和掩码),防止 libgomp 在超过 90 核的生产宿主机上为每个核开线程、把 worker 的RLIMIT_DATA打穿导致pthread_create返回EAGAIN、进而exit(1)静默崩溃(Dockerfile 注释记录了 bc3 某次安装中 285 个 worker 因此被杀的真实事故); - 健康检查:
hotcell-health从容器内探测控制 socket——因为容器内network: none不适用外部探针。
cell 的 Gemfile 刻意保持极短("每个 gem 都在爆炸半径内、每次请求都要付钱"),只有hotcell-server与activestorage-hotcell-server(均锁定0.4.1),并明确"hotcell-client和rails不属于这里"。而 operations/active_storage.rb 逐个 require 需要的操作(不加载 ImageMagick / Poppler 操作,因为镜像里没有这些工具),并把图片分析的文件上限从 gem 默认的 48MB 抬到 256MB——"48MB 装不下一张 4800 万像素的手机照片",同时屏蔽openslide(fork 的 worker 里会 segfault sqlite)与tiff(Fizzy 从不读取)加载器。
6.6 部署:Kamal + 两个钩子让 cell 自动跟上
生产部署使用 Kamal,两个容器一起部署:
bin/kamal deploy -d <destination>Kamal 把 cell 当作一个accessory(自己的镜像、自己的生命周期)。两个钩子让 cell 与应用永远保持同步,部署者无需操心:
- pre-build(saas/.kamal/hooks/pre-build)执行
saas/hotcell/bin/check --publish:如果 registry 里没有本次提交 pin 的 cell 镜像,就按宿主平台构建并推送。发生在部署锁之前、不做任何 ssh; - pre-deploy(saas/.kamal/hooks/pre-deploy)执行
saas/hotcell/bin/check --reboot:如果宿主机上跑的不是 pin 的镜像,就地重启该 accessory。发生在锁内、应用启动之前。
两者都从被部署的提交(KAMAL_VERSION)取 pin,所以bin/kamal rollback会把 cell 一起回退;两者都尊重--hosts与--roles,只影响指定的机器。如果故意要在坏掉的 cell 之上部署,用SKIP_HOTCELL_CHECKS=1跳过两个检查。
手动检查则用:
saas/hotcell/bin/check <destination> # 报告 cell 状态与修复方式 saas/hotcell/bin/check <destination> --apply # 直接修复退出码直接命名修复动作:2= 需要构建并推送;3= 只需重启 accessory;1= 检查自身无法判断。脚本(saas/hotcell/bin/check)的实现要点:
- 检查分两步,先本地问 registry(零 ssh 成本)
docker manifest inspect判断 pin 的 tag 是否存在,再随机抽查一个宿主机上 accessory 实际跑的镜像(pin 按目标漂移而不按宿主机漂移,全量 ssh 生产要一分钟); --version=SHA从指定提交取 pin(而非工作树),这正是 rollback 能落回对应 cell 的原因;--hosts=a,b收窄时只问/只重启这些机器;- 宁可失败也不猜:docker 未登录、ssh 不通都会判为"无法判断"(退出码 1),并明确告知修复方式。README 点出理由:"因为
docker manifest inspect认证失败就让人重建一个已发布镜像,是最昂贵的失败模式。"
6.7 修改 cell 的标准工作流
任何被镜像拷入的saas/hotcell/内容变化,或Gemfile.saas.lock中 hotcell gem 的版本移动,都意味着一个新镜像。CI 会在你欠镜像时给出提醒:saas/test/lib/hotcell_accessory_test.rb 的 "the pinned image is the one this tree builds" 用例会失败——如果deploy.yml的 pin 与当前树构建出的 tag 不再一致。注意该测试是对 Kamal 生成的docker run命令断言而非对 YAML 断言(Kamal 自己会加 flag,文件里正确的东西可能到 daemon 变成重复参数),并且遍历全部目标(beta除外——它是需要BETA_NUMBER的模板)。
完整改动流程(README 的 5 步):
构建,它会顺带 pin saas/config/deploy.yml:
saas/hotcell/bin/build --platform=linux/amd64跑测试:
bin/rails test saas/test/lib/hotcell_accessory_test.rb一起提交:两份 lockfile、pin、以及
saas/hotcell/下所有改动;部署每个目标。钩子看到新 pin 不在 registry → 推送 → 重启 cell → 部署应用:
bin/kamal deploy -d <destination>验证目标:
/hotcellz返回OK;/hotcellz/test(staff 专属)四项检查全过;Prometheus 中每台宿主机hotcell_up == 1;Loki 中{service_name="hotcell"}无WARN/ERROR行。
build是唯一必须手动执行的步骤,因为它写下的 pin 必须属于你的提交:tag 是内容哈希,改变内容的那个提交必须携带它;check --publish拒绝推送未提交的 pin。
相关脚本职责分明(全部位于 saas/hotcell/bin/):
- image:被 build 与 deploy 共同 source,保证两者对镜像名认知一致;
- build:把 cell 的 lockfile 锁到应用锁文件的 hotcell 版本、构建镜像、把 saas/config/deploy.yml 的 pin 更新为内容哈希 tag。不碰 registry;
- push:把已构建镜像推送到 registry,并校验架构必须是 amd64(否则宿主机无法运行,accessory 会 crash-loop 且应用侧毫无报错)。不可逆的一半,所以从 build 中独立出来;重启仍然显式、不在其中;
- check:前述的检查/修复脚本。
6.8 为什么这样设计(三个核心决策)
tag 是内容哈希,而不是 git revision。tag 取Dockerfile、Gemfile、Gemfile.lock、config.rb、operations/*.rb的 SHA-256 前 12 位(见 saas/hotcell/bin/image)。原因:若用 commit 命名,那个"升级 gem 并 pin 结果"的提交永远无法命名自己——amend 它会改变 pin 本该指向的 SHA。相同字节得到相同 tag,改到镜像不包含的东西则完全不影响它。
build 把 cell 的 lockfile 锁到应用的 hotcell 版本。因为客户端与服务器差一个版本,就是每次请求的protocol失败。应用的 lockfile 是唯一事实来源:先移动应用里的 gem,再构建。build 通过awk分别从Gemfile.saas.lock与 cell 的Gemfile.lock读hotcell-core版本(两侧都以精确约束依赖它),不一致时用sed改写(并刻意删除 CHECKSUMS 行,避免旧版本的 sha256 挂到新版本上导致 bundler 校验拒绝),再重新bundle install并重读验证——"不能信任一次 sed"。
tag 不可变,没有latest。部署不会更新 accessory;kamal accessory reboot拉取的是该 tag 当前指向的内容,一个漂移的 tag 会让"宿主机跑什么"取决于它上次重启的时间。这也是为什么只升级 gem 的部署也必须重启 cell——pre-deploy 钩子替你做了这件事。
七、部署环境总览
Fizzy 托管版用 Kamal 部署,部署前需要配置好 1Password CLI 以读取机密,之后部署就是一句:
bin/kamal deploy -d <destination>各环境定位(详见 saas/README.md 与各deploy.<destination>.yml):
- Production(https://app.fizzy.do/):正式环境,blob 存储使用 FlashBlade 桶;
- Beta(https://beta1.fizzy-beta.com):主要用于产品功能测试,与生产共用同一套数据库与 Active Storage 配置;目前有 1 个 beta 环境,部署命令
bin/kamal deploy -d beta1。注意 saas/config/deploy.beta.yml 是模板,必须提供BETA_NUMBER环境变量;beta1是唯一真实存在的编号目标(saas/.kamal/secrets.beta2至secrets.beta4符号链接是历史遗留,不构成真实目标); - Staging(https://app.fizzy-staging.com/):主要用于基础设施变更测试,使用与生产类似但完全独立的数据库与 Active Storage 配置。
各目标的环境差异通过 saas/config/deploy.beta1.yml、saas/config/deploy.staging.yml、saas/config/deploy.production.yml 对基础 saas/config/deploy.yml 做深合并(deep-merge)而来——这正是 accessory 测试要遍历所有目标的原因:目标文件深合并时数组是替换而非合并。
生产监控与可观测性由yabeda体系支撑:引擎在 saas/lib/fizzy/saas/engine.rb 的fizzy_saas.yabedainitializer 中安装了 SolidQueue、ActionCable、GVL、ActiveSupportCache、SolidCache、HotCell 等指标插件,并定义fizzy_replica_stale计数与fizzy_replica_wait直方图(saas/lib/fizzy/saas/metrics.rb),配合事务固定中间件(TransactionPinning)度量读副本追主库的等待时间。
八、维护模式:把生产离线
要因维护把生产下线,在负载均衡器上通过knife ssh执行kamal-proxy stop:
knife ssh 'hostname:fizzy-lb-*' "sudo docker exec fizzy-load-balancer kamal-proxy stop fizzy --message='Sorry! Fizzy is undergoing some maintenance and will be back shortly.'"访问 https://app.fizzy.do/ 验证维护页已生效;恢复时执行:
knife ssh 'hostname:fizzy-lb-*' 'sudo docker exec fizzy-load-balancer kamal-proxy resume fizzy'九、License
fizzy-saas 以 O'Saasy License 发布(saas/LICENSE.md),与开源 Fizzy 的许可相互独立——采用本引擎部署托管服务前,请先确认该许可对自身业务的适用性。
小结
围绕 saas/README.md 这条主线,本指南把 SaaS 模式的四件事讲透了:一是saas:enable/saas:disable切换机制与它对 bundle、数据库、Kamal 配置的连锁影响;二是 Stripe 与原生推送在本地开发时的凭据注入模式(eval "$(...)"+ 1Password);三是 Hotcell 把附件处理隔离到无网络旁路容器的完整架构——从两个环境变量开关、/hotcellz两级健康检查,到内容哈希 tag、双钩子自动发布/重启的部署链路;四是各环境与维护模式的运维要点。对任何需要"让第三方二进制处理不可信文件"的 Rails 应用来说,saas/hotcell/ 这套"最小镜像 + 内容哈希 pin + 自动对账"的 cell 方案,都是一份值得直接借鉴的参考实现。
【免费下载链接】fizzyKanban as it should be. Not as it has been.项目地址: https://gitcode.com/GitHub_Trending/fizzy2/fizzy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考