GitDiagram 部署故障转移指南:基于 Dockerfile 与 railway.json 的离线 Railway 恢复方案
【免费下载链接】gitdiagramFree, simple, fast interactive diagrams for any GitHub repository项目地址: https://gitcode.com/GitHub_Trending/gi/gitdiagram
GitDiagram 是一套"单一 Next.js 应用、单一线上部署目标"的系统:前端与全部后端 Route Handler 同进程运行,日常流量由 Vercel 承载。当 Vercel 必须被临时替换时,仓库内检入了Dockerfile与railway.json两份离线恢复资产,可在 Railway 上重建同一套完整服务。本文以 docs/deployment-failover.md 为骨架,结合仓库源码逐层拆解这套恢复方案的设计边界、完整操作步骤、健康检查机制与回切流程,帮助读者在真实故障场景中安全、可验证地完成平台切换。
一、方案定位:离线恢复配方,不是常驻备用
先从定位说起。GitDiagram 只有一个应用实现和一个线上部署目标:
- 线上生产环境(Live production):Vercel 在
gitdiagram.com上同时提供前端页面和每一个后端 Route Handler(/api/generate/*、/api/healthz、/api/diagram-preview、/api/diagram-state、/api/browse-index等); - 离线恢复选项(Offline recovery option):
Dockerfile与railway.json可以把同一份应用打包部署到 Railway,仅当 Vercel 日后必须被替换时才启用。
需要明确的事实边界是:当前没有已部署的 Railway 服务、没有已连接的 Railway 源、没有 Railway 域名、也没有 Railway DNS 记录。Railway 不在正常请求路径上,不接收任何流量,也不会产生任何常驻计算成本。这套恢复资产的意义在于"随时可用、按需拉起",而非"双活容灾"。
从 docs/dev-setup.md 的部署章节也可以交叉印证:"The same source can be redeployed to Railway later throughDockerfileandrailway.json. Those files are an offline recovery recipe, not a live standby."(同一份源码日后可通过Dockerfile与railway.json重新部署到 Railway;这些文件是离线恢复配方,而非实时备胎。)两处文档描述一致,恢复路径是项目部署策略中的一等公民。
二、保留的恢复资产:生产级 Docker 路径全貌
该方案不引入任何第二套后端实现。文档明确强调:"This is not the old Python/FastAPI backend. No Python service or second API implementation is required."(这不是旧的 Python/FastAPI 后端,不需要 Python 服务或第二套 API 实现。)恢复后暴露的 UI、Route Handler、图编译器(graph compiler)、配额逻辑、取消协议(cancellation protocol)与持久化代码,与 Vercel 上运行的是完全相同的代码。
2.1 Dockerfile:三阶段构建 + 非 root 运行
仓库根目录的 Dockerfile 采用经典的三阶段模式:
FROM oven/bun:1.3.14-alpine AS dependencies WORKDIR /app COPY package.json bun.lock ./ COPY patches ./patches RUN bun install --frozen-lockfile FROM oven/bun:1.3.14-alpine AS builder WORKDIR /app COPY --from=dependencies /app/node_modules ./node_modules COPY . . ENV NEXT_TELEMETRY_DISABLED=1 ENV RAILWAY_DOCKER_BUILD=1 RUN bun run build FROM node:22-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 ENV HOSTNAME=0.0.0.0 ENV PORT=3000 RUN addgroup --system --gid 1001 nodejs \ && adduser --system --uid 1001 nextjs \ && mkdir .next \ && chown nextjs:nodejs .next COPY --from=builder --chown=nextjs:nodejs /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 CMD ["node", "server.js"]逐层解读这份恢复路径对应的各项要求:
- 依赖层:使用
oven/bun:1.3.14-alpine执行bun install --frozen-lockfile,与仓库 package.json 中packageManager: "bun@1.3.14"和engines: { "bun": ">=1.3.14 <2" }的约束保持一致;patches目录(如 patches/minimatch@3.1.5.patch)会被完整复制,保证patchedDependencies在构建中生效。 - 构建层:设置
RAILWAY_DOCKER_BUILD=1环境变量,这是触发 Next.jsstandalone输出的开关(见下文 2.3 节)。bun run build对应 package.json 中的"build": "bun run --bun next build",即用 Bun 作为构建运行时。 - 运行层:切换到
node:22-alpine,用node server.js直接运行 Next.js standalone 产物。HOSTNAME=0.0.0.0让服务监听容器全部网卡,PORT=3000是默认值,最终会被 Railway 注入的PORT覆盖(文档要求"respects the platform-providedPORT",即尊重平台注入的端口)。 - 非 root 运行:构建阶段创建
nextjs(uid/gid 1001)系统用户,COPY时用--chown=nextjs:nodejs归属文件,最后USER nextjs切换身份,满足生产容器最小权限原则。
2.2 railway.json:平台侧的部署与健康检查契约
仓库根目录的 railway.json 是 Railway 平台的部署配置文件:
{ "$schema": "https://railway.com/railway.schema.json", "build": { "builder": "DOCKERFILE", "dockerfilePath": "Dockerfile" }, "deploy": { "healthcheckPath": "/api/healthz", "healthcheckTimeout": 300, "restartPolicyType": "ON_FAILURE", "restartPolicyMaxRetries": 5 } }build.builder = "DOCKERFILE":Railway 直接使用仓库内的Dockerfile构建镜像,不需要额外的平台构建脚本;deploy.healthcheckPath = "/api/healthz":部署健康检查指向应用自身的健康检查路由(见 2.4 节),只有该路由返回成功,Railway 才认为部署健康;healthcheckTimeout = 300:健康检查超时上限为 300 秒,给冷启动、依赖连接留足时间;restartPolicyType = "ON_FAILURE"与restartPolicyMaxRetries = 5:失败时自动重启,最多重试 5 次。
2.3 next.config.js:standalone 输出的条件触发
next.config.js 中的关键一行验证了 2.1 节的说法:
...(process.env.RAILWAY_DOCKER_BUILD === "1" ? { output: "standalone" } : {}),也就是说,output: "standalone"只在RAILWAY_DOCKER_BUILD=1时启用——这正是 Dockerfile 构建阶段设置该变量的原因。Vercel 正常部署不受影响,仍走 Vercel 自身的构建链路(vercel.json 中声明bunVersion: "1.x",让 Vercel 以 Bun 运行 Route Handler)。这也解释了为什么这套恢复路径"不需要浏览器 CORS 开关、不需要公开的后端选择器":整个 Next.js 应用连同其standalone输出整体搬迁,前端与 API 始终保持同源。
2.4 /api/healthz:部署健康检查与就绪探测
Railway 的健康检查指向/api/healthz。它的实现位于 src/app/api/healthz/route.ts:
export const runtime = "nodejs"; export const dynamic = "force-dynamic"; export async function GET() { const readiness = await checkReadiness(); return NextResponse.json( { ok: readiness.ok, status: readiness.ok ? "ok" : "unavailable", checks: readiness.checks }, { status: readiness.ok ? 200 : 503, headers: { "Cache-Control": "no-store" }, }, ); }注意两点:runtime = "nodejs"与dynamic = "force-dynamic"确保该路由走 Node 运行时且不被缓存;返回体中的checks对象让排障者能一眼定位是哪个依赖出了问题。
真正的探测逻辑在 src/server/readiness.ts 的checkReadiness()中,包含五类检查:
| 检查项 | 含义 | 实现依据 |
|---|---|---|
configuration | 必需的配置项是否齐全 | 逐一readRequiredEnv校验R2_ACCOUNT_ID、R2_ACCESS_KEY_ID、R2_SECRET_ACCESS_KEY、R2_PUBLIC_BUCKET、R2_PRIVATE_BUCKET、UPSTASH_REDIS_REST_URL、UPSTASH_REDIS_REST_TOKEN、CACHE_KEY_SECRET(定义见 src/server/storage/config.ts) |
provider | AI 提供方密钥是否存在 | 根据getProvider()判断需要OPENAI_API_KEY还是OPENROUTER_API_KEY(对应AI_PROVIDER=openai/openrouter) |
publicStorage | 公开 R2 存储桶可访问 | checkR2Bucket()走一次与业务相同的认证 GetObject 路径(src/server/storage/r2.ts) |
privateStorage | 私有 R2 存储桶可访问 | 同上,探测私有桶 |
redis | Upstash Redis 可连接 | checkUpstashConnection()发送PING并期望返回PONG(src/server/storage/upstash.ts) |
只有当Object.values(checks).every(Boolean)全部为真时,ok才为true,路由返回 200;任何一个依赖不可用则返回 503。这与 src/app/api/healthz/route.test.ts 中的测试契约一致:"returns 200 only when required dependencies are ready"、"returns 503 when a required dependency is unavailable"。因此,/api/healthz不只是"进程活着"的探针,而是依赖就绪度探针——这在恢复场景中极其重要,它能防止 Railway 在 R2 或 Upstash 凭据未配置时就被错误地判定为部署成功。
三、按需重建 Railway 的完整操作流程
文档明确告诫:"Do not run these commands during normal operation."(正常运行时不要执行这些命令。)以下命令仅用于真实的恢复场景。整个过程分为七个步骤。
第 1 步:锁定精确提交并通过质量门禁
# 检出与生产一致的精确提交,并本地跑通质量门禁 bun run lint bun run typecheck bun run test bun run build对应 package.json 中的lint、typecheck、test、build脚本。之所以要求"精确生产提交",是因为恢复部署必须与 Vercel 上运行的应用行为完全一致,任何未经过验证的提交都不应进入恢复路径。测试套件覆盖了确定性图编译器(Mermaid 解析器契约测试)、API 路由测试、取消与配额测试、存储并发测试以及浏览器渲染安全测试(见 docs/dev-setup.md),这些测试全部通过后再进入下一步。
第 2 步:关联或创建 Railway 项目
# 关联当前目录到已存在的空 Railway 项目 railway link # 或新建一个名为 gitdiagram 的项目 railway init --name gitdiagram要求"空项目"是为了避免与旧环境残留混淆。
第 3 步:创建一个未连接(unconnected)的服务
railway add --service gitdiagram-api这一步创建的是未连接 GitHub 源的服务。文档在第 5 步会再次强调这一点:railway up不会把服务连接到 GitHub,也不会自行创建公网域名——所有流量入口都是显式、临时的。
第 4 步:注入必需的环境变量(密钥走 stdin)
从 .env.example 添加必需变量,密钥值通过标准输入传入,避免进入 shell 历史:
railway variable set VARIABLE_NAME --stdin --service gitdiagram-api恢复必需的核心变量分为四组:
| 组别 | 变量 | 说明 |
|---|---|---|
| R2 对象存储 | R2_ACCOUNT_ID、R2_ACCESS_KEY_ID、R2_SECRET_ACCESS_KEY | S3 兼容客户端的端点和凭据(见 src/server/storage/r2.ts 中https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com的拼接逻辑) |
| R2 存储桶 | R2_PUBLIC_BUCKET、R2_PRIVATE_BUCKET | 公开桶承载可公开访问的产物,私有桶承载受限内容 |
| 缓存与签名 | CACHE_KEY_SECRET | 用于缓存键的签名/哈希 |
| Upstash Redis | UPSTASH_REDIS_REST_URL、UPSTASH_REDIS_REST_TOKEN | 共享配额、取消、分布式锁与故障状态的存储后端(见 src/server/storage/upstash.ts) |
同时还需配置 AI 提供方(AI_PROVIDER=openai加OPENAI_API_KEY,或AI_PROVIDER=openrouter加OPENROUTER_API_KEY),否则readiness.ts中的provider检查将失败,/api/healthz会返回 503。可选的生成控制项包括OPENAI_MODEL、OPENAI_COMPLIMENTARY_GATE_ENABLED、OPENAI_COMPLIMENTARY_DAILY_LIMIT_TOKENS、OPENAI_COMPLIMENTARY_MODEL_FAMILY、OPENROUTER_MODEL、OPENROUTER_SITE_URL、OPENROUTER_APP_NAME;可选 GitHub 认证项包括GITHUB_PAT/GITHUB_PATS(令牌池)与 GitHub App 的GITHUB_APP_ID(或GITHUB_CLIENT_ID)、GITHUB_PRIVATE_KEY、GITHUB_INSTALLATION_ID;浏览器分析可选NEXT_PUBLIC_POSTHOG_KEY。默认值示例(AI_PROVIDER=openai、OPENAI_MODEL=gpt-5.6-terra)可在 .env.example 中查到。
第 5 步:上传并部署当前检出
railway up --service gitdiagram-api再次强调:railway up既不连接 GitHub,也不自行创建公网域名。部署过程实际执行 Dockerfile 的构建,产物即 2.1 节描述的 standalone 应用。
第 6 步:添加临时域名并逐项验证
添加一个临时 Railway 域名后,按顺序验证以下五项核心能力:
- 健康检查:
/api/healthz返回 200 且checks全为true(对应 src/server/readiness.ts 的五项就绪检查); - 成本估算:
/api/generate/cost正常工作(实现见 src/app/api/generate/cost/route.ts); - 一次小规模流式生成:
/api/generate/stream能完整走通流式响应(实现见 src/app/api/generate/stream/route.ts 与 src/features/diagram/sse.ts); - 取消协议:生成过程中的取消请求能被正确处理(实现见 src/server/generate/cancellation.ts 与 src/app/api/generate/cancel/route.ts);
- 持久化的图状态:
/api/diagram-state能读写已持久化的图状态(实现见 src/server/storage/diagram-state.ts)。
这五项恰好覆盖了恢复路径必须证明的完整闭环:部署健康 → 计量正确 → 核心业务可用 → 生命周期控制可用 → 状态持久化可用。
第 7 步:仅在验证通过后做显式路由决策
文档给出的原则是:"Keep Vercel intact until the incident is resolved."(在事故解决前保持 Vercel 完好。)只有上述五项全部通过,才把流量显式切到临时域名对应的入口,并且随时保留回切能力。
为什么恢复不需要三样东西
因为整个 Next.js 应用整体搬迁,恢复过程不需要:
- 浏览器 CORS 开关:前端与 API 同进程、同源部署,不存在跨源问题;
- 公开的后端选择器:不存在"前端选后端"的开关,代码与 Vercel 完全一致;
- 数据迁移:R2 拥有图产物(diagram artifacts),Upstash 拥有共享配额、取消、锁与故障状态——两者都是平台无关的外部服务,随域名切换自动生效。
这正是"一个应用实现、一个部署目标、零数据搬迁"方案的容灾优势。
四、回切 Vercel 的流程
事故解决后按四个步骤回切,与恢复流程严格对称:
# 1. 验证 Vercel 侧健康 curl https://gitdiagram.com/api/healthz # 期望 200 # 并做一次小规模生产生成验证 # 2. 若路由被改动,将 gitdiagram.com 恢复到预期的 Vercel 部署 # 3. 移除临时 Railway 域名,删除 Railway 服务 railway domain --remove # 移除临时域名 railway delete --service gitdiagram-api # 删除恢复服务 # 4. 确认清理完成: # - Railway 项目下已无任何服务 # - DNS 区域中已无任何 Railway 记录回切完成后,仓库内检入的恢复文件(Dockerfile、railway.json)仍然保留,供下次事故使用,而不需要保持 Railway 的计算资源、部署版本或公网端点常驻——这既降低了成本面,也消除了"无人维护的备用环境反而成为攻击面"的风险。
五、运维边界与安全检查要点
综合文档与源码,这套恢复方案有若干必须遵守的边界:
- Railway 不参与正常请求路径:没有已部署服务、没有连接源、没有域名、没有 DNS 记录,因此不存在意外流量或重复扣费风险;
- 健康检查是就绪探针而非存活探针:
/api/healthz会真实探测 R2 双桶与 Upstash 连通性(src/server/readiness.ts),配置不全会返回 503,healthcheckTimeout300 秒足够覆盖冷启动; - 密钥不得进入 shell 历史:一律使用
railway variable set VARIABLE_NAME --stdin --service gitdiagram-api; - 域名与流量是显式的:
railway up不建域名、不连 GitHub,公网入口必须手动添加并同样手动移除; - 回切以验证为前提:Vercel 侧先过
/api/healthz与一次生产生成,再动路由,最后清理 Railway 残留; - 构建开关有明确的触发条件:
output: "standalone"仅在RAILWAY_DOCKER_BUILD=1时生效(next.config.js),日常 Vercel 构建路径完全不受影响。
六、总结
GitDiagram 的部署故障转移方案可以用一句话概括:"一套代码、一份 Dockerfile、一个健康检查,按需在 Railway 上重建与 Vercel 完全等价的生产服务,验证通过后显式切流,事故结束后完整回切并清理残留。"这套方案没有引入第二套后端、没有常驻备用环境、没有数据迁移成本,全部恢复能力都沉淀在 Dockerfile、railway.json 和/api/healthz就绪检查(src/app/api/healthz/route.ts、src/server/readiness.ts)中。对于采用单一 PaaS 部署的产品,这套"离线恢复配方"的定位与实施步骤,是一个值得复用的容灾设计样本。
【免费下载链接】gitdiagramFree, simple, fast interactive diagrams for any GitHub repository项目地址: https://gitcode.com/GitHub_Trending/gi/gitdiagram
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考