本文介绍了基于monorepo架构的CI/CD流程,强调上线的稳定性和可追溯性。作者将流程分为CI和Deploy两条线,确保PR阶段高效检查,发布阶段仅构建受影响的应用。通过路径检测、并行构建和Docker镜像优化,提升构建速度并确保密钥安全。最终,方案具备扩展性,适合未来的多环境部署与测试。
之前花了一点时间把当前blog项目改成了 基于的monorepo架构,然后经过慢慢摸索之后总结出当前的整个发布构建的流程
我写这套 CI/CD 的出发点很朴素:主分支一合并就回不去。所以我更关心的是——上线这件事能不能“稳”和“可追溯”,而不是把流程堆得很复杂。
这篇文章完全基于当前仓库里的真实实现(你可以直接对照):
我把目标拆成四件事,基本也是我踩坑后最在意的四件事:
1. 构建要稳定:同样的代码,换个 runner 或换个时间,不应该“玄学失败”。
2. 上线要可追溯:线上出问题时,能快速定位到是哪个 commit、哪个镜像。
3. 速度要够用:monorepo 每次全量构建太贵,PR 反馈也不能拖太久。
4. 密钥要守规矩:能不进镜像就别进镜像;能不进 bundle 就别进 bundle。
我刻意把它拆成两条线:
- CI(质量线):只管“这段代码能不能合并”。
- Deploy(发布线):只管“合并后怎么把它安全地推到线上”。
这样做的好处是:PR 不用背负 Docker 构建的成本;线上发布也不用夹杂 lint/typecheck 这些噪音。
CI 配置在 .github/workflows/ci.yml,我最终把它收敛到两类 job:
pnpm lint`pnpm check-types缓存这块我用到以下组合
.turbo:Turbo 任务复用apps/blog/.next/cache:Next 的构建缓存(主要为了后续可能的 build-check 扩展/或你也可以把它理解为仓库对 Next 构建缓存的准备)TURBO_API / TURBO_TOKEN / TURBO_TEAMDeploy 在 `.github/workflows/deploy.yml`。
monorepo 最大的浪费是:改了一个小地方,却把所有 app 都重新打包。这里我用 `dorny/paths-filter@v3` 做路径规则。
以 blog 为例,触发条件不只包含 `apps/blog/**`,还包含它依赖的 packages,以及 Dockerfile/workflow 自身:
这件事非常关键:否则你改了 `packages/ui`,blog 可能根本不会发布。
变更检测之后我会生成一个 matrix:
- 自动模式:只构建被判定为 “changed=true” 的 app
- 手动模式:`workflow_dispatch` 可以指定某个 app;如果手动触发但没指定 app,就默认全构建(方便运维场景)
另外我加了并发互斥,避免同一个 app 在同一分支上被连续 push 触发时“旧的覆盖新的”:
我用 docker/metadata-action@v5 自动生成标签:
sha tag(真正用于追溯和回滚)latest(只在默认分支启用,当成“默认指针”即可)回滚建议很简单:别用 latest 回滚,用 sha。
`turbo.json` 里我最看重的是 `globalEnv` 和 build 的 inputs/outputs:
- `globalEnv` 里放的是“会影响构建产物语义”的变量(例如 `DATABASE_URI`、`PAYLOAD_SECRET`、`PREVIEW_SECRET`、`TURNSTILE_SECRET_KEY`、`NEXT_PUBLIC_S3_MEDIA_DOMAIN` 等)。这些变了,缓存就应该失效。
- build outputs 明确排除了 `.next/cache`,因为它容易引入不稳定数据。
- build inputs 里包含 `.env*`,意味着你本地/环境配置变更会触发重建。
一句话讲人话:我希望缓存加速,但不希望缓存替我撒谎。
`docker/blog.Dockerfile` 我做了四个取舍,基本都是为“可重复、可上线、镜像别太胖”服务:
turbo prune:
turbo prune blog --docker 先把构建上下文裁小pnpm + BuildKit cache:
.next/cache 也用 cache mount构建期变量分两类:
ARG:主要放 NEXT_PUBLIC_* 以及少量非敏感、确实会影响构建的配置(例如 BLOG_NAME、CDN_URL 这类)secrets:敏感内容一律走 BuildKit secrets(不会写进镜像层)standalone 运行镜像:
.next/standalone + .next/staticnextjs)我刻意强调一句边界:BuildKit secrets 只解决“构建过程不泄露”,不解决“运行时拿不到”。
所以运行时需要的变量,还是要由 Dokploy 注入。
我自己判断“构建期 vs 运行期”的标准很简单:
NEXT_PUBLIC_*:几乎一定是构建期(会进入浏览器 bundle)在这个仓库里:
- 构建期通过 `build-args` 传入的主要是:
NEXT_PUBLIC_SERVER_URL、NEXT_PUBLIC_TURNSTILE_SITE_KEY
- 敏感信息在构建期用 BuildKit secrets(例如 DATABASE_URI、OPENAI_API_KEY 等)。
- 真正运行时必须依赖的变量,最终都应该在 Dokploy 的环境变量里配置(避免“镜像里带密钥”)。
如果你只记一句话:能运行时注入就运行时注入;不要把服务端密钥塞进 build-arg,更不要塞进 NEXT_PUBLIC
我现在的发布方式是:镜像推到 GHCR 之后,GitHub Actions 用 curl -X POST 触发 Dokploy webhook。
这里我做了一个“偏实用”的选择:webhook 失败不让流水线红(continue-on-error: true)。原因是:
当然,这也意味着你最好在 Dokploy 侧把告警/日志/健康检查做好,否则会出现“构建绿了但线上没更”的错觉。
我保留了 latest 是为了“默认指向”,但回滚时我只用 sha tag:
如果你后面要做多环境(staging/prod),再补一层固定 tag(比如 branch-main / branch-develop)会更顺手。
我不想把它包装得很玄乎,它值钱的点其实就三条:
后续要扩展也很自然:加 staging/prod、加 smoke test、加审批/灰度,都能在这套骨架上继续长。