pnpm workspace 下有 apps/web、apps/admin、apps/docs,push 之后线上只有 apps/web 更新了。admin 和 docs 要么没动、要么挂到了错的子域。更糟的是 Vercel 上看到三个 “Project”,但其中两个不管 push 什么源码都不会触发构建。这就是 monorepo 拆分部署的经典陷阱。修复几乎不在代码层面,而在每个 Vercel/Netlify/Cloudflare 项目对 monorepo 根目录、Ignored Build Step、turbo filter 作用域的配置。多数团队一套配置只踩一次坑,下次又忘。
常见原因
按踩坑频率排序。
1. 三个项目都把”根目录”设成了 ./
每个 Vercel 项目必须指向自己的子目录(apps/web、apps/admin、apps/docs)。三个项目都填仓库根,就都在构建默认的同一个 app。两个跟第一个产物一样,只有第一个项目的域名生效。
如何识别:每个 Vercel 项目 → Settings → Root Directory,全为空或 ./,就是它。
2. Ignored Build Step 没配好
Vercel 的 “Ignored Build Step” 用类似 git diff --quiet HEAD^ HEAD ./apps/admin 的命令。路径写错、写成仓库根、或者根本没配,要么每次 push 都全量重建,要么对应 app 永远不重建。
如何识别:改了 apps/admin 里的文件 push 上去,Vercel 显示 “Build was skipped due to Ignored Build Step”,但 apps/admin 源码确实变了。
3. Turborepo filter 作用域不对
构建命令是 turbo run build --filter=web...(从仓库根跑)。web... 只构建 web 和它的依赖,admin 和 docs 一次都不会被碰。
如何识别:构建日志里 Tasks: 1 successful, 1 total,但你期望是 3。turbo run build --dry 也确认 filter 命中只有一个 app。
4. pnpm-workspace.yaml 的 packages glob 漏了
packages: ["apps/web", "packages/*"] —— admin/docs 不在 workspace 里,install + build 整个跳过。它们根本没成”真”包。
如何识别: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 页。Project Settings 的 Output Directory 是错的。
6. 多个项目重名导致部署互踩
平台上两个 Vercel 项目同名,第二个 deploy 会盖掉第一个的域名 alias。
如何识别:两个项目都叫 frontend;push 一个,另一个的域名跳到了错的 app。改名后症状会跟着移动。
7. app 内 .vercelignore 或 next.config.js 排除了关键文件
apps/admin/.vercelignore 写了广泛模式(手误或从别项目复制),把自己的页面目录给排除了,部署上去就是一个空壳。
如何识别:apps/admin/.vercelignore 里有 pages/ 或 app/ 这样的广泛模式。对照 app 实际结构核查。
开始排查前
- 列出 monorepo 里所有可部署 app:
ls apps/。 - 每个 app 确认走哪个部署 provider、对应哪个 “Project”。
- 从面板上拿到每个项目的 Root Directory 设置。
- 拿到每个项目的 Build Command 和 Output Directory。
- 记录包管理器和 workspace 配置(
pnpm-workspace.yaml、package.json的 workspaces 字段)。
需要收集的信息
- 每个可部署 app 的:provider、project name、root directory、build command、output directory。
- 仓库根的
package.jsonworkspaces或pnpm-workspace.yaml内容。 - 用了 Turborepo 的话,
turbo.json内容。 - 每个项目的 “Ignored Build Step” 命令(如果有)。
- 每个项目的域名 alias(Vercel: Settings → Domains)。
- 每个项目最近的部署日志,记录构建了什么、跳过了什么。
分步修复
按性价比排序。
步骤 1:核对每个项目的根目录
Vercel → 每个项目 → Settings → General → Root Directory。
预期:
apps/web → frontend 项目
apps/admin → admin 项目
apps/docs → docs 项目
任何一个为空或 ./,立刻改对并触发一次新部署。这一个设置是最常见的元凶。
步骤 2:每个项目的 Build Command 只指向自己
Turborepo:
# 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 一致,不是目录名。
步骤 3:每个项目配好正确的 Ignored Build Step
Vercel 项目设置里:
# 这个 app 涉及的文件没变就跳过部署
git diff --quiet HEAD^ HEAD -- apps/admin packages/ui
-- 后面是路径。要包括这个 app 的目录 以及 它依赖的共享包。只查 app 目录的话,packages/ui 改了就不会触发重建,那就漏掉了真正该重建的场景。
Turborepo:
npx turbo-ignore @org/admin
turbo-ignore 会沿依赖图判断,更靠谱。
步骤 4:每个项目的输出目录与框架对齐
| 框架 | 输出目录 |
|---|---|
| Next.js | .next(或自动识别) |
| Astro | dist |
| Vite | dist |
| SvelteKit | build |
| Remix (Vite) | build/client |
项目设置里 Output Directory 留空通常即可(Vercel 按 framework preset 自动识别)。除非你的构建写到了别处。
步骤 5:workspace packages 列表覆盖所有可部署 app
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
或 package.json:
{
"workspaces": ["apps/*", "packages/*"]
}
用 apps/* glob 比枚举更安全,新 app 自动纳入。
步骤 6:每个项目独立测试一次部署
每个项目:
- 在对应 app 里改一行无关紧要的注释。
- push 到该项目跟踪的分支。
- 验证启动了一次新部署,并且线上 URL 真的更新了。
某个项目没触发,说明 Ignored Build Step 错;触发了但线上没更新,是 output directory 或域名 alias 错。
步骤 7:每个项目设置明确的生产域名
Vercel → 每个项目 → Settings → Domains:
apps/web → www.example.com
apps/admin → admin.example.com
apps/docs → docs.example.com
每个项目只持有一个生产域名。共用就会出现原因 #6 的互踩。相关 alias 切换见 canonical domain change。
验证
- 每个 app 改个 trivial 文件,只触发该 app 对应的项目部署。
- 每个 app 的生产 URL 提供的正是该 app 的内容(不是 404 或别的 app)。
- 根目录跑
turbo run build --dry列出的图与每个 filter 的预期一致。 - 没有任何项目 Root Directory 还是
./(除非项目里确实只有一个 app)。 - 构建日志显示上传的是预期的 Output Directory。
长期预防
- 在
apps/README.md或顶层 CONTRIBUTING 里写清楚 project 与 app 的映射。 - 用
turbo-ignore取代手写 diff 命令,依赖图它跟得对。 package.jsonname统一加上@org/作用域,filter 不出错。- CI 加一条检查:
apps/下新增目录但没对应部署项目就 fail。 - monorepo 下绝不把 Vercel 项目根目录设成
./,必须是子目录。 - 新 app 时先建部署项目,再写代码,避免漏配。
常见坑
- Ignored Build Step 设成
git diff --quiet HEAD^ HEAD不带路径 —— 永远返回 true,永远不部署。 - 作用域包用
--filter=web而不是--filter=@org/web—— Turbo 静默什么都不构建。 - 用 Vercel 的 “duplicate” 复制项目却忘了改根目录 —— 两个项目跑同一个 app。
- 忘了
apps/admin依赖packages/ui,ignore 检查必须覆盖两者 —— 不该重建的乱建、该重建的不建。 - 从仓库根
vercel deploy然后纳闷为啥只有根项目收到部署。相关 CLI 坑见 vercel build failed。
常见问答
Q: 能不能把整个 monorepo 部署到一个 Vercel 项目?
理论可行但不推荐。要写 next.config.js 把子路由代理到子 app,还会丢掉按 app 隔离的回滚和 preview。标准做法仍是一个可部署 app 对应一个 Vercel 项目。
Q: Vercel 是按项目收费的,三个 app 就是三个项目了?
是的,付费档下三个项目就按三份用量算。多数团队觉得值,因为每个 app 隔离的 preview 已经回本。
Q: 我们用 Nx 不是 Turbo,规则一样吗?
思路一样,filter 语法不同。构建命令换成 nx run web:build 或 nx affected --target=build --base=HEAD^。“Ignored Build Step” 模式完全一致。
Q: 能用 Vercel 部署非 web 包(CLI、Lambda)吗?
不行 —— Vercel 期望的是 web 框架。CLI 发 npm 或 GitHub Releases,Lambda 用 AWS 或 SST。相关部署目标错配见 firebase deploy permission denied 和 vercel build failed。