在 Data Clerk 上部署
此应用运行在 Data Clerk 上,这是一个由运营商(人,联系方式见 PROJECT.md)管理的小型 Kubernetes 集群。此仓库的拥有者通常不是基础设施人员。他们永远不需要接触服务器、Kubernetes 或云控制台:他们推送到 main,集群完成其余工作。 你的工作是保持这种状态:在仓库内工作,遵守以下契约,当确实需要集群侧操作时,给运营商写一个清晰的请求,而不是临时凑合。
用通俗的语言向用户解释(例如“你的网站将在大约五分钟内更新”),而不是使用 Kubernetes 术语。
从这里开始
- 阅读
deploy-data-clerk/PROJECT.md。它包含此项目的值:公共 URL、部署文件夹、镜像名称、端口、迁移命令、限制、运营商是谁。下面所有提到“来自 PROJECT.md”的地方都指该文件。如果 PROJECT.md 中的文件夹名称与deploy-data-clerk/不同(旧项目使用deploy/),则使用 PROJECT.md 中指定的名称。 - 没有
PROJECT.md,或者它仍然包含__PLACEHOLDER__值?项目尚未完成接入:请遵循references/new-project.md。 - 编写或更改 Dockerfile、选择如何组织应用结构、或者技术栈不是 Node?请阅读
references/stacks.md(任何语言,全栈 vs 分离前端 + API,FastAPI,Go)。 - 在机器上执行第一个机密相关任务之前,确保工具正常工作:
sops --version和测试解密(sops decrypt <部署文件夹>/secrets.env > /dev/null)。如果任一失败,请遵循references/setup-tools.md。
变更如何到达线上站点
| 变更内容 | 发生什么 | 上线时间 |
|---|---|---|
代码(推送到 main) | GitHub Actions 构建 Docker 镜像并推送;集群注意到新的镜像标签并滚动更新 | ~3–7 分钟(构建时间 + ~1 分钟) |
secrets.env(推送到 main) | 集群每 5 分钟重新读取部署文件夹,解密,更新应用的环境变量并重启应用 | ~5 分钟 |
migrations/ 中的新文件 | 在新版本启动前自动运行 | 随代码部署一起 |
main 之外的任何内容 | 不执行任何操作。分支和 PR 不会被部署 | — |
滚动更新是谨慎的:新版本在旧版本旁边启动,先运行迁移,只有在新版本通过健康检查后才会切换流量。如果构建、迁移或健康检查失败,旧版本将继续服务,站点保持在线。部署失败意味着“变更未上线”,而不是“站点宕机”。
要检查部署:gh run list --workflow "Deploy to cluster" --limit 3(或 GitHub 上的 Actions 选项卡),然后打开公共 URL。要在没有代码变更的情况下重新部署:gh workflow run "Deploy to cluster",它将构建一个全新的标签,因此应用会以全新的环境重启。
契约(保证部署正常工作)
每条规则的存在是因为违反它会导致静默失败或数据丢失。遵守它们,并告诉用户某个请求是否会违反规则。
- 监听
$PORT(PROJECT.md 中的端口,由集群设置),在0.0.0.0上,而不是localhost。 - 镜像从
<部署文件夹>/Dockerfile构建,目标为linux/amd64,并以非 root 用户运行。不能是 root,不能使用 1024 以下的端口,不能有额外的 Linux 能力。 - 镜像标签格式为
YYYYMMDDHHMMSS-<短sha>。集群部署编号最高的标签,并静默忽略任何不符合此格式的标签。永远不要更改.github/workflows/deploy-cluster.yml中的标签步骤。 - 容器的磁盘在每次部署和重启时被清空。 永远不要将用户上传、SQLite 文件或任何有价值的内容存放在本地磁盘上。将数据放在数据库中;对于文件(照片、视频),向运营商请求对象存储。
- 健康检查: 集群探测 PROJECT.md 中的健康路径(推荐
/healthz)。使其快速返回 200,不要进行数据库调用或页面渲染,否则慢速的数据库会使集群认为应用已死。 DATABASE_URL来自集群。 永远不要将其放在secrets.env中(会覆盖真实值),也永远不要在镜像构建时设置它。- 运行时 vs 构建时。
secrets.env仅在运行时到达应用。镜像在 GitHub Actions 中构建,不包含它。任何在构建时内联到前端的变量(VITE_*、NEXT_PUBLIC_*、REACT_APP_*)不能来自secrets.env:在服务器端运行时读取这些值并传递给页面,或者(如果不保密)在代码中提交它们。 - 应用在缺少时自行生成的机密(身份验证/会话密钥、加密盐、推送密钥):在
secrets.env中设置它们。否则每次重启都会生成新的,导致用户登出、推送订阅中断等。 - 迁移必须对旧版本安全。 大约一分钟内,两个版本都针对新 schema 运行。可以自由添加列和表。重命名或删除需要两次部署:首先停止使用,然后移除。详细信息在
references/database.md中。
环境变量和机密
所有运行时配置都位于 <部署文件夹>/secrets.env 中,使用 sops 加密提交。仅加密值;键名保持可读。使用 sops 本身进行更改。永远不要将其解密到可能提交的文件中,也永远不要将秘密值打印到聊天或日志中。
sops set deploy-data-clerk/secrets.env '["STRIPE_SECRET_KEY"]' '"sk_live_..."'
sops unset deploy-data-clerk/secrets.env '["OLD_KEY"]'
sops decrypt deploy-data-clerk/secrets.env | cut -d= -f1
然后提交并推送到 main;大约 5 分钟后上线。当用户需要提供值时(来自某个控制面板的 API 密钥),让他们自己运行 sops set 行,或者仅将值粘贴到他们自己的终端中。完整指南(密钥、存储位置、轮换、注意事项)在 references/secrets.md 中。
数据库
Postgres 18,由运营商管理。应用通过 DATABASE_URL 获得自己的数据库和用户。Schema 更改是通过迁移文件实现的,这些文件在部署时自动应用(references/database.md)。
备份: 每天 UTC 时间 03:00 进行完整备份,加上每次更改的连续日志,存储在 Google Cloud Storage 的服务器之外。运营商可以将数据库恢复到过去 7 天内的任意时刻。因此,如果数据被删除或迁移出错,不要惊慌,也不要试图手动修复:记录发生的时间,并向运营商请求恢复(模板在 references/troubleshooting.md 中)。恢复会回滚整个数据库,因此该时刻之后所做的更改将丢失。
你的代码可能需要的网络事实
- 流量路径:访问者 → Cloudflare → Traefik(集群入口)→ 应用。HTTPS 证书是自动的。应用本身在
$PORT上使用明文 HTTP;X-Forwarded-Proto: https表示原始请求是 HTTPS。信任代理头部,不要在应用中重定向到 HTTPS(会导致循环)。 - 真实访问者 IP: 读取
CF-Connecting-IP头部。X-Forwarded-For包含 Cloudflare 地址,因此基于它的速率限制会将所有访问者放入一个桶中。 - Traefik 添加以下头部:HSTS(2 年,preload)、
X-Content-Type-Options: nosniff、X-Frame-Options: SAMEORIGIN(站点无法在其他域名上以 iframe 嵌入)。额外头部(CSP、用于SharedArrayBuffer的 COOP/COEP、允许 iframe)由运营商根据需要设置。 - 入口不压缩响应。Cloudflare 压缩常见的文本类型;要完全控制,在应用中压缩或提供预压缩资源。
- 长连接(SSE、WebSockets)没问题,但 Cloudflare 会断开空闲约 100 秒的连接。每约 30 秒发送一次心跳,并在客户端重新连接。
- 出站: 应用可以调用任何公共互联网端点上的任何端口。这包括 LLM API、Stripe、电子邮件、S3,以及在其公共端点上的托管数据库或 Redis(Neon、Supabase、Atlas、Upstash)。它无法访问私有网络(VPC 对等连接、PrivateLink、私有 IP)或集群上的其他项目。入站流量仅通过公共 URL 进入。
- 第三方服务: 将它们的密钥和 URL 放在
secrets.env中。如果服务需要 IP 白名单,集群有一个静态出站 IP:向运营商询问(未公开),并告诉用户该 IP 与集群上的其他项目共享。服务器位于美国中部,因此外部数据库优先选择美国中部/美国东部区域(欧洲数据库每个查询增加约 100 毫秒)。外部数据库是用户自己的:Data Clerk 的备份不覆盖它。对于慢速 LLM 调用,请流式传输响应或将其作为后台任务运行;等待时间超过约 100 秒的浏览器请求将被切断。 - 资源有上限(PROJECT.md 中的内存/CPU)。超过内存限制会杀死并重启应用。如果发生这种情况,请向运营商请求更多空间,而不是在代码中对抗它。
- 应用运行一个副本(一个副本)。不要依赖于在部署后仍然存在的内存中状态,将会话保留在数据库中或签名 cookie 中。
只有运营商才能做的事情
用户没有集群访问权限,这是设计使然。给运营商写一个简短的请求(模板见 references/troubleshooting.md),用于:
新域名或子域名 · 更多内存/CPU · 文件/对象存储 · 数据库恢复 · 应用日志或崩溃原因 · 更改安全头部 · 更改迁移或启动命令、端口或健康路径 · 计划任务(cron) · 额外服务(Redis、队列) · 同一项目中的第二个应用 · 接入新仓库。
更多详细信息
references/stacks.md:适用于任何语言的通用契约;全栈 vs SPA + API vs 静态;Node、Python FastAPI、Go 和其他技术栈。references/new-project.md:接入仓库(要添加的文件、需要向运营商请求的内容、每个技术栈的 Dockerfile)。references/secrets.md:sops 深入、密钥文件、轮换、构建时变量。references/database.md:迁移、连接字符串、备份、数据修复。references/setup-tools.md:在 macOS、Linux 和 Windows 上安全安装 sops/git/gh。运行scripts/install-tools.sh或scripts/install-tools.ps1。references/troubleshooting.md:“我的变更未上线”、“站点宕机”、常见错误、运营商请求模板。assets/:Dockerfile 模板(Node、Python、Go)以及部署工作流、kustomization.yaml、.sops.yaml、.gitattributes和PROJECT.md的模板。