monorepo 部署只发了一个 app —— 排查与修复

monorepo 里有三个可部署 app,Vercel 只发了一个。修好每个项目的 Root Directory、Skip deployment 开关和 Turborepo filter 作用域 —— 含 2026 年 6 月的准确菜单路径。

pnpm workspace 下有 apps/webapps/adminapps/docs,push 之后线上只有 apps/web 更新了。admin 和 docs 要么没动、要么挂到了错的子域。更糟的是 Vercel 上看到三个 “Project”,但其中两个不管 push 什么源码都不会触发构建。

最快的修法:打开每个 Vercel 项目,进 Settings → Build and Deployment → Root Directory,确认每个项目都指向自己的子目录(apps/webapps/adminapps/docs)。如果两个项目共用了根目录(./)或指向了同一个子目录,它们就在构建同一个 app,最后只有一个域名生效。大约 70% 的情况都是这一个设置。剩下 30% 是 Turborepo --filter 把别的 app 漏掉了、Skip deployment 开关导致永远不重新触发构建、或者输出目录配错部署成了空壳。

这就是 monorepo 拆分部署的经典陷阱。修复几乎不在代码层面,而在每个 Vercel 项目对仓库根目录、变更检测、turbo filter 作用域的配置。

先判断你属于哪一类

先拿症状对一下这张表,再跳到下面对应的原因。

症状最可能的原因章节
两个 app 内容完全一样,只有一个域名能用Root Directory 共用或配错原因 1
app 源码改了但 Vercel 显示 “skipped” / 根本没部署Skip deployment 或 Ignored Build Step 太严原因 2
构建日志显示 Tasks: 1 successful, 1 total 而不是 3Turborepo --filter 作用域太窄原因 3
pnpm ls -r 没列出缺失的那个 appworkspace glob 漏了它原因 4
构建”成功”但 URL 是 404 页Output Directory 配错原因 5
push 一个 app 把另一个 app 的域名带跑了项目重名 / 域名互踩原因 6
app 部署成了没有页面的空壳app 内 .vercelignore 把自己页面排除了原因 7

常见原因

按踩坑频率排序。

1. 多个项目共用了同一个 Root Directory

每个 Vercel 项目必须指向自己的子目录(apps/webapps/adminapps/docs)。两个或更多项目填了仓库根(./)或同一个子目录,就都在构建同一个默认 app。重复的几个产物相同,谁占着线上域名谁生效。

如何识别:每个 Vercel 项目 → Settings → Build and Deployment → Root Directory,全为空或 ./ 就是它。(Vercel 在新版项目设置里把这个设置从旧的 General 标签挪到了 Build and Deployment;如果你的面板还在 General 下显示,同一个字段也是它。)

2. 这个 app 永远不会重新触发构建

Vercel 上有两套互相独立的变更检测机制,行为不一样。选错了那套(或配错了)就是源码确实改了却显示 “skipped” 的原因。

  • Skip unaffected projects(现代默认)。 截至 2026 年 6 月,Vercel 会自动跳过 monorepo 里没变化的项目,除非它的源码、它的某个内部依赖、或只影响它依赖的 lockfile 改动发生了变化。对走 GitHub 且用 npm / yarn / pnpm / Bun workspaces 的项目默认开启,而且它不占用 concurrent build slot。前提是每个 workspace 包在 package.json 里有唯一name,且跨包依赖显式声明 —— 如果 apps/admin 用了 packages/ui 却没把它列为依赖,Vercel 看不到这条连线,ui 改了就会跳过 admin。
  • Ignored Build Step(手动逃生口)。 你自己写的一条 shell 命令(或 npx turbo-ignore)。按 Vercel 的规则,退出码 0SKIP 构建,退出码 1 或更大会 PROCEED(继续构建)。被 Ignored Build Step 取消的构建仍然计入你的 concurrent build 和 deployment 限额,所以 app 多的仓库用自动跳过更划算。

如何识别:改了 apps/admin 里的文件 push 上去,看 Vercel。看到 “Build was skipped due to Ignored Build Step” 但 apps/admin 源码确实变了,说明 Ignored Build Step 的路径写错了。要是压根没出现部署、而你也没配 Ignored Build Step,多半是自动跳过没看到这次改动 —— 通常是 name 缺失/重复,或内部依赖没声明。

3. Turborepo filter 作用域不对

构建命令是 turbo run build --filter=web...(从仓库根跑)。web... 只构建 web 和它的依赖,admin 和 docs 一次都不会被碰。

如何识别:构建日志里 Tasks: 1 successful, 1 total,但你期望是 3。跑 turbo run build --dry(或 --dry=json)看 packages / 任务列表,确认 filter 命中只有一个 app。

4. workspace glob 漏了那个 app

packages: ["apps/web", "packages/*"] —— admin/docs 不在 workspace 里,install + build 整个跳过。它们根本没成”真”包,Vercel 的自动跳过会把它们的改动当成全局变更(或者直接漏掉)。

如何识别pnpm ls -r 没列出 admin/docs。pnpm install 不会把它们 symlink 进 node_modules

5. 输出目录配错

Next.js 是 .next、Astro dist、Vite dist、SvelteKit build。配错的话 Vercel 构建成功但上传了空目录或错目录,部署”成功”实际上是 404 站点。

如何识别:构建日志 success,部署 URL 显示 Vercel 默认 404 页。Settings → Build and Deployment 里的 Output Directory 被覆盖成了错的值。

6. 项目重名导致部署互踩

平台上两个 Vercel 项目同名,或者你在多个项目里复用了同一个生产域名,第二个 deploy 就会盖掉第一个的域名 alias。

如何识别:存在两个都叫 frontend 的项目,或两个项目抢同一个域名。push 一个,另一个的域名跳到了错的 app。改名后症状会跟着移动。

7. app 内 .vercelignore 排除了关键文件

apps/admin/.vercelignore 写了广泛模式(手误或从别项目复制),把自己的页面目录给排除了,部署上去就是一个空壳。

如何识别apps/admin/.vercelignore 里有 pages/app/ 这样的广泛模式。对照 app 实际结构核查。

开始排查前

每个可部署 app 都把下面这几项收集齐 —— 这五个事实并排一摆,多数修法就一目了然:

  • 走哪个部署 provider、对应哪个 “Project”(ls apps/ 列全)。
  • 每个项目的 Root Directory(在 Settings → Build and Deployment 看)。
  • 每个项目的 Build Command 和 Output Directory。
  • 包管理器和 workspace 配置(pnpm-workspace.yaml,或 package.jsonworkspaces)。
  • turbo.json 内容,以及每个项目若有 Ignored Build Step 的命令。
  • 每个项目的域名 alias(Settings → Domains),以及最近的部署日志(记录构建了什么、跳过了什么)。

分步修复

按性价比排序。

步骤 1:核对每个项目的 Root Directory

Vercel → 每个项目 → Settings → Build and Deployment → Root Directory

预期:

apps/web   → frontend 项目
apps/admin → admin 项目
apps/docs  → docs 项目

任何一个为空或 ./,立刻改对并触发一次新部署。这一个设置是最常见的元凶。导入时同一字段是在第一次部署前用 Root Directory 旁的 Edit 按钮设置的。

步骤 2:让 Vercel 的自动跳过来干活(然后核对依赖图)

对多数团队,正确做法是根本不写 Ignored Build Step,直接用内置的自动跳过。先确认前提:

  • 项目连的是 GitHub,且用 npm / yarn / pnpm / Bun workspaces。
  • workspace 里每个包在 package.json 里都有唯一name
  • 跨包依赖显式声明,比如 apps/admin 引用了 packages/ui 就在 apps/admin/package.json 里写 "@org/ui": "workspace:*"

跳过开关在 Settings → Build and Deployment → Root Directory → Skip deployment(设为 Enabled 才会跳过没变化的项目)。因为它不占用 concurrent build slot,app 多的仓库比 Ignored Build Step 更省。

步骤 3:每个项目的 Build Command 只指向自己

每个项目的构建命令最终都应只落到自己这一个 app。在 Vercel 上,只要 Root Directory 设对了,filter 会自动推断,可以直接写:

turbo run build

需要写明的话:

# apps/web 项目
cd ../.. && turbo run build --filter=@org/web

# apps/admin 项目
cd ../.. && turbo run build --filter=@org/admin

pnpm 过滤:

pnpm --filter @org/web run build

--filter 的值必须与对应 app package.json 里的 name 一致,不是目录名。turbo 在 Vercel 上是全局可用的,不必把它加进依赖。

步骤 4:如果你确实要用 Ignored Build Step,就写对它

只有在自动跳过没法描述你的仓库时(非 JS 的 workspace、自定义分支规则)才动它。记住退出码规则:退出 0 跳过,退出 1+ 构建。

# 这个 app 或它的共享依赖没变就跳过部署。
# git diff 退出 1(有变化)-> 构建;退出 0(无变化)-> 跳过。
git diff --quiet HEAD^ HEAD -- apps/admin packages/ui

-- 后面是路径。要包括这个 app 的目录 以及 它依赖的共享包。只查 app 目录的话,packages/ui 改了就不会触发重建,那就漏掉了真正该重建的场景。

Turborepo 优先用 turbo-ignore,它会沿依赖图判断:

npx turbo-ignore --fallback=HEAD^1

这正是 Vercel 默认设的命令。--fallback=HEAD^1 很关键:全新分支上没有”上一次部署的 SHA”可比对,单跑 turbo-ignore 会保守地全量构建。--fallback=HEAD^1 让它改和父提交比对,新分支也能跳过没受影响的 app。

步骤 5:每个项目的 Output Directory 与框架对齐

框架输出目录
Next.js.next(或自动识别)
Astrodist
Vitedist
SvelteKitbuild
Remix (Vite)build/client

Settings → Build and Deployment 里 Output Directory 留空通常即可(Vercel 按 framework preset 自动识别)。除非你的构建写到了别处。用了 Turborepo Remote Caching 的话,还要确认 turbo.json 里的 outputs 和 preset 对齐(比如 Next.js 用 [".next/**", "!.next/cache/**"]),否则缓存命中时会报找不到构建产物。

步骤 6:workspace packages 列表覆盖所有 app

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

package.json

{
  "workspaces": ["apps/*", "packages/*"]
}

apps/* glob 比枚举更安全,新 app 自动纳入。workspace 定义之外的任何改动都会被 Vercel 的跳过逻辑当成全局变更、重建所有 app,所以把可部署 app 都放进 glob 里。

步骤 7:每个项目独立测试一次部署

每个项目:

  1. 在对应 app 里改一行无关紧要的注释。
  2. push 到该项目跟踪的分支。
  3. 验证启动了一次新部署,并且线上 URL 真的更新了。

某个项目没触发,说明它的变更检测(skip 开关或 Ignored Build Step)错了;触发了但线上没更新,是 Output Directory 或域名 alias 错。

步骤 8:每个项目设置明确的生产域名

Vercel → 每个项目 → Settings → Domains

apps/web   → www.example.com
apps/admin → admin.example.com
apps/docs  → docs.example.com

每个项目只持有一个生产域名。共用就会出现原因 6 的互踩。相关 alias 切换见 canonical domain change

如果 app 之间要跨 preview 互相引用(前端调用自己的后端 preview),用 Vercel 的 Related Projects:在该 app 的 vercel.json 里加 { "relatedProjects": ["prj_..."] }(每个 app 最多 3 个),再从 VERCEL_RELATED_PROJECTS 环境变量读 URL,而不是把 host 写死。

怎么确认修好了

  • 每个 app 改个 trivial 文件,只触发该 app 对应的项目部署 —— 别的项目纹丝不动。
  • 每个 app 的生产 URL 提供的正是该 app 的内容(不是 404 或别的 app)。
  • 根目录跑 turbo run build --dry 列出的图与每个 filter 的预期一致。
  • 没有任何项目 Root Directory 还是 ./(除非项目里确实只有一个 app)。
  • 构建日志显示上传的是预期的 Output Directory(看部署详情的 “Building” 段)。
  • 改一个共享包(packages/ui)会重建所有依赖它的 app,且只重建这些。

长期预防

  • apps/README.md 或顶层 CONTRIBUTING 里写清楚 project 与 app 的映射。
  • 优先用 Vercel 的自动跳过,少手写 diff 命令;非要写脚本就用 turbo-ignore --fallback=HEAD^1
  • 每个包都给唯一的 name,统一加 @org/ 作用域,并显式声明跨包依赖,跳过依赖图才准。
  • CI 加一条检查:apps/ 下新增目录但没对应部署项目就 fail。
  • monorepo 下绝不把 Vercel 项目 Root Directory 设成 ./,必须是子目录。
  • 新 app 时先建部署项目,再写代码,避免项目漏配部署目标。

常见坑

  • Ignored Build Step 设成 git diff --quiet HEAD^ HEAD 不带路径 —— 永远退出 0,于是永远跳过部署(不是大家以为的”永远构建” —— 退出 0 就是跳过)。
  • 作用域包用 --filter=web 而不是 --filter=@org/web —— Turbo 静默什么都不构建。
  • 用着自动跳过,却让两个包共用同一个 name、或某个内部依赖没声明 —— Vercel 建不出依赖图,要么过度构建、要么跳错 app。
  • 用 Vercel 的 “duplicate” 复制项目却忘了改 Root Directory —— 两个项目跑同一个 app。
  • 全新分支上单跑 npx turbo-ignore 会全量构建,因为没有 SHA 可比对;加上 --fallback=HEAD^1
  • 从仓库根 vercel deploy 然后纳闷为啥只有根项目收到部署。相关 CLI 坑见 vercel build failed

常见问答

Q: app 的源码改了,Vercel 却还是跳过了部署,为什么?

通常两种原因。没配 Ignored Build Step 的话,是 Vercel 自动跳过没能把这次改动连到该 app —— 几乎都是包 name 重复/缺失,或内部依赖没声明(比如 apps/admin 引用了 packages/ui 却没在 package.json 里列出来)。配了 Ignored Build Step 的话,是命令退出了 0(意味着跳过):检查 -- 后面的路径。

Q: 现在还需要 Ignored Build Step 吗?

对多数走 GitHub 的 JS monorepo,不需要。自动的 “Skip unaffected projects” 已经覆盖了,而且和 Ignored Build Step 不同,它不占用 concurrent build slot、也不计入部署限额。只有自动跳过没法描述你的仓库时(非 JS workspace,或基于分支的自定义规则)才上手动的 Ignored Build Step(或 turbo-ignore)。

Q: 能不能把整个 monorepo 部署到一个 Vercel 项目?

理论可行但不推荐。要写路由/代理把子路由映射到子 app,还会丢掉按 app 隔离的回滚和 preview。标准做法仍是一个可部署 app 对应一个 Vercel 项目。

Q: 我们用 Nx 不是 Turbo,规则一样吗?

思路一样,filter 语法不同。构建命令换成 nx run web:buildnx affected --target=build --base=HEAD^。Root Directory 和变更检测步骤完全一致;自动跳过对任意 task runner 下的 npm/yarn/pnpm/Bun workspaces 也都有效。

Q: 能用 Vercel 部署非 web 包(CLI、Lambda)吗?

不行 —— Vercel 期望的是 web 框架。CLI 发 npm 或 GitHub Releases,Lambda 用 AWS 或 SST。相关部署目标错配见 firebase deploy permission deniedvercel build failed

标签: #排查 #monorepo #turbo #Vercel #部署