Eavan's Blog LogoEavan's Blog
TAGSFRIENDSBOOKSSEARCH
TAGS
FRIENDS
BOOKS
SEARCH
Eavan's Blog LogoEAVAN
·

心如止水,字如清风

流水不争先 ·

个人monorepo 项目 CI/CD 全链路:GitHub Actions + Docker + Dokploy

12/27/2025
7 分钟

作者

标签

精选

OpenAI

AI 摘要 (由 openai/gpt-4o-mini 生成)

本文介绍了基于monorepo架构的CI/CD流程,强调上线的稳定性和可追溯性。作者将流程分为CI和Deploy两条线,确保PR阶段高效检查,发布阶段仅构建受影响的应用。通过路径检测、并行构建和Docker镜像优化,提升构建速度并确保密钥安全。最终,方案具备扩展性,适合未来的多环境部署与测试。

2.44s
~2092 tokens

前言

这个项目的初衷,源于我个人项目管理上的一个“老大难”问题。我的手上同时维护着好几个项目:博客、API 服务、还有一些实验性的小应用。起初它们各自安好地待在独立的 GitHub 仓库里,但很快,麻烦就找上了门。

比如,我想更新一下 ESLint 的规则,或者升级 TypeScript 的配置,就必须在三四个仓库里重复同样的操作。更头疼的是,我写了一个感觉不错的 React 组件,想在另一个项目里复用,除了复制粘贴或者发布成 npm 包之外,似乎没有更优雅的办法。

为了解决这种配置不统一、组件难共享的困境,我把目光投向了 Monorepo。将所有项目都放在一个仓库里,共享的配置(eslint, tsconfig)可以提升到根目录,公共组件可以轻松地被任何一个应用引用。这正是我想要的!

然而,新的挑战接踵而至。当我把所有项目都迁移到 Monorepo 之后,我发现 CI/CD 变得异常棘手:我只想修改后端代码,为什么前端应用也要跟着重新构建和部署一遍?

这显然是不能接受的。于是,我开始了一段探索之旅,目标就是打造一个能精准识别代码变更、实现智能部署的 Monorepo CI/CD 流程。这篇文章,就是我整个探索和实践过程的完整记录。

本方案基于网站图标Turborepo的monorepo架构,然后经过慢慢摸索之后总结出当前的整个发布构建的流程

这篇文章完全基于当前仓库里的真实实现(你可以直接对照):

1. 这套链路到底在解决什么问题

我把目标拆成四件事,基本也是我踩坑后最在意的四件事:

1. 构建要稳定:同样的代码,换个 runner 或换个时间,不应该“玄学失败”。

2. 上线要可追溯:线上出问题时,能快速定位到是哪个 commit、哪个镜像。

3. 速度要够用:monorepo 每次全量构建太贵,PR 反馈也不能拖太久。

4. 密钥要守规矩:能不进镜像就别进镜像;能不进 bundle 就别进 bundle。

2. 从 push 到上线:我这套流水线长什么样

我刻意把它拆成两条线:

- CI(质量线):只管“这段代码能不能合并”。

- Deploy(发布线):只管“合并后怎么把它安全地推到线上”。

这样做的好处是:PR 不用背负 Docker 构建的成本;线上发布也不用夹杂 lint/typecheck 这些噪音。

3. CI:PR 阶段我只做“最值钱”的检查

CI 配置在 .github/workflows/ci.yml,我最终把它收敛到两类 job:

  • pnpm lint`
  • pnpm check-types

缓存这块我用到以下组合

  • pnpm store:安装依赖别每次重来
  • .turbo:Turbo 任务复用
  • apps/blog/.next/cache:Next 的构建缓存(主要为了后续可能的 build-check 扩展/或你也可以把它理解为仓库对 Next 构建缓存的准备)
  • Turborepo 远程缓存:TURBO_API / TURBO_TOKEN / TURBO_TEAM

4. Deploy:只构建“真的受影响”的应用

Deploy 在 `.github/workflows/deploy.yml`。

4.1 变更检测:我用的是 paths-filter(并把依赖也算进去)

monorepo 最大的浪费是:改了一个小地方,却把所有 app 都重新打包。这里我用 `dorny/paths-filter@v3` 做路径规则。

以 blog 为例,触发条件不只包含 `apps/blog/**`,还包含它依赖的 packages,以及 Dockerfile/workflow 自身:

这件事非常关键:否则你改了 `packages/ui`,blog 可能根本不会发布。

4.2 matrix:并行构建 + 可手动点名

变更检测之后我会生成一个 matrix:

- 自动模式:只构建被判定为 “changed=true” 的 app

- 手动模式:`workflow_dispatch` 可以指定某个 app;如果手动触发但没指定 app,就默认全构建(方便运维场景)

另外我加了并发互斥,避免同一个 app 在同一分支上被连续 push 触发时“旧的覆盖新的”:

4.3 镜像标签:我保留了 sha + latest,但回滚只认 sha

我用 docker/metadata-action@v5 自动生成标签:

  • 分支 tag(便于环境绑定/排查)
  • PR tag(便于预览/验证)
  • sha tag(真正用于追溯和回滚)
  • latest(只在默认分支启用,当成“默认指针”即可)

回滚建议很简单:别用 latest 回滚,用 sha。

5. Turbo:缓存不是“越多越好”,而是“哪些变化必须重跑”

`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*`,意味着你本地/环境配置变更会触发重建。

一句话讲人话:我希望缓存加速,但不希望缓存替我撒谎。

6. Dockerfile:把 monorepo “裁剪”成一个可以上线的 blog

`docker/blog.Dockerfile` 我做了四个取舍,基本都是为“可重复、可上线、镜像别太胖”服务:

turbo prune:

  • turbo prune blog --docker 先把构建上下文裁小

pnpm + BuildKit cache:

  • pnpm store 用 BuildKit cache mount(配合 deploy.yml 的 GHA cache backend)
  • .next/cache 也用 cache mount

构建期变量分两类:

  • ARG:主要放 NEXT_PUBLIC_* 以及少量非敏感、确实会影响构建的配置(例如 BLOG_NAME、CDN_URL 这类)
  • secrets:敏感内容一律走 BuildKit secrets(不会写进镜像层)

standalone 运行镜像:

  • 运行镜像只复制 .next/standalone + .next/static
  • 以非 root 用户启动(nextjs)

我刻意强调一句边界:BuildKit secrets 只解决“构建过程不泄露”,不解决“运行时拿不到”。

所以运行时需要的变量,还是要由 Dokploy 注入。

7. 环境变量:最容易写错、也最容易泄露的地方

我自己判断“构建期 vs 运行期”的标准很简单:

  • NEXT_PUBLIC_*:几乎一定是构建期(会进入浏览器 bundle)
  • 只在服务端运行时读取的(例如 DB、私钥、第三方 API key):必须是运行期

在这个仓库里:

- 构建期通过 `build-args` 传入的主要是:

NEXT_PUBLIC_SERVER_URL、NEXT_PUBLIC_TURNSTILE_SITE_KEY

- 敏感信息在构建期用 BuildKit secrets(例如 DATABASE_URI、OPENAI_API_KEY 等)。

- 真正运行时必须依赖的变量,最终都应该在 Dokploy 的环境变量里配置(避免“镜像里带密钥”)。

如果你只记一句话:能运行时注入就运行时注入;不要把服务端密钥塞进 build-arg,更不要塞进 NEXT_PUBLIC

8. Webhook:让 CI 系统别去碰服务器

我现在的发布方式是:镜像推到 GHCR 之后,GitHub Actions 用 curl -X POST 触发 Dokploy webhook。

这里我做了一个“偏实用”的选择:webhook 失败不让流水线红(continue-on-error: true)。原因是:

  • 镜像已经推上去了,它是确定的产物
  • 部署失败更像“运行平台的问题”,需要平台侧告警、重试、回滚

当然,这也意味着你最好在 Dokploy 侧把告警/日志/健康检查做好,否则会出现“构建绿了但线上没更”的错觉。

9. 回滚:我只信一个东西——sha

我保留了 latest 是为了“默认指向”,但回滚时我只用 sha tag:

  • 不需要重新构建
  • 指向明确
  • 可审计

如果你后面要做多环境(staging/prod),再补一层固定 tag(比如 branch-main / branch-develop)会更顺手。

10. 写在最后:这套方案哪里“值”

我不想把它包装得很玄乎,它值钱的点其实就三条:

  • PR 阶段快、且把高命中率问题挡住
  • 发布阶段只构建受影响的 app,并且能追溯到 sha
  • 密钥从镜像里拿出来,至少不会在最基础的地方翻车

后续要扩展也很自然:加 staging/prod、加 smoke test、加审批/灰度,都能在这套骨架上继续长。

如未标记非原创,转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,未经站长允许不得对文章文字内容进行修改演绎,不得用于商业目的。
本文采用CC BY-NC-SA 4.0 - 非商业性使用 - 相同方式共享 4.0 国际进行许可。

评论