Monorepo Deploy Only Ships One App Out of Several

Your monorepo has three deployable apps but Vercel only deploys one — usually a root directory misconfig, ignored build command, or Turbo filter that swallows the others.

You have a pnpm workspace with apps/web, apps/admin, and apps/docs. You push, and only apps/web shows up on the live domain. Admin and docs deployed silently to nothing or to the wrong subdomain. Or worse: Vercel happily shows three “Projects” but two of them never rebuild even when their source changes. This is the monorepo split-deploy trap. The fix is rarely about your code; it is about how each Vercel/Netlify/Cloudflare project is configured against the monorepo’s root directory, ignored build steps, and turbo filter scope. Most teams hit this exactly once per setup, then forget the next time.

Common causes

Ordered by how often each catches teams.

1. All three projects share the same “root directory” of ./

Each Vercel project must point at its app’s subdirectory (apps/web, apps/admin, apps/docs). If all three are set to repo root, they all build the same default app. Two of them will produce the same output as the first — but the first one’s domain wins.

How to spot it: Open each Vercel project → Settings → Root Directory. If they are all blank or ./, this is it.

2. Ignored build step prevents rebuild for unchanged apps

Vercel’s “Ignored Build Step” feature uses a command like git diff --quiet HEAD^ HEAD ./apps/admin. If that path is wrong, set to repo root, or absent, every push rebuilds everything — but if it is overly restrictive, the project never rebuilds.

How to spot it: Push a change to apps/admin, watch Vercel: deployment skipped with “Build was skipped due to Ignored Build Step”. Yet apps/admin source did change.

3. Turborepo filter scope mismatch

Build command is turbo run build --filter=web... from repo root. The web... filter only builds web and its dependencies. Admin and docs never get touched.

How to spot it: Build log shows Tasks: 1 successful, 1 total when you expected 3. Filter list in turbo run build --dry confirms only one app in scope.

4. pnpm-workspace.yaml packages glob excludes the missing app

packages: ["apps/web", "packages/*"] — admin/docs are not in the workspace, so the install + build skip them entirely. They never become “real” packages.

How to spot it: pnpm ls -r does not list admin/docs. pnpm install does not symlink them into node_modules.

5. Wrong output directory configured

For Next.js it is .next; for Astro dist; for Vite dist; for SvelteKit build. If the project’s output directory is misconfigured, Vercel finishes the build but uploads an empty/wrong dir, and the deploy “succeeds” with a 404 site.

How to spot it: Build log says success, deploy URL serves Vercel’s default 404 page. Output directory field in Vercel Project Settings is wrong.

6. Each app sharing same project name causes deploy collisions

If two Vercel projects share the same name on the platform, the second one’s deploys overwrite the first’s domain aliases.

How to spot it: Two projects named frontend exist; pushing one redirects the other’s domain to the wrong app. Rename one and the symptom moves around.

7. App-level .vercelignore or next.config.js excludes critical files

A .vercelignore in apps/admin/ that excludes its own pages directory (typo or copy-pasted from another project) causes the app to deploy as an empty shell.

How to spot it: apps/admin/.vercelignore contains broad patterns like pages/ or app/. Cross-check against the app’s structure.

Before you start

  • List all deployable apps in the monorepo: ls apps/.
  • For each, identify which deploy provider and which “Project” maps to it.
  • Get the exact root directory configured for each project from the dashboard.
  • Confirm the build command and output directory configured for each project.
  • Capture the package manager and workspace config (pnpm-workspace.yaml, package.json workspaces field).

Information to collect

  • For each deployable app: provider, project name, root directory, build command, output directory.
  • Repo-root package.json workspaces field or pnpm-workspace.yaml content.
  • turbo.json content if Turborepo is in use.
  • The “Ignored Build Step” command per project (if set).
  • Domain aliases per project (in Vercel: Settings → Domains).
  • Recent deploy logs for each project showing what was built and skipped.

Step-by-step fix

Ordered by ROI.

Step 1: Audit each project’s root directory

Vercel → For each project → Settings → General → Root Directory.

Expected:

apps/web   → frontend project
apps/admin → admin project
apps/docs  → docs project

If any is blank or ./, set it correctly and trigger a new deploy. This single setting is the most common cause.

Step 2: Audit and align the build command per project

Per project, the build command should target only that app. For Turborepo:

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

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

Or use pnpm filtering:

pnpm --filter @org/web run build

The --filter value MUST match the package name in that app’s package.json, not the directory name.

Step 3: Configure the Ignored Build Step correctly per project

In each Vercel project settings:

# Skip deploy if no files in this app changed
git diff --quiet HEAD^ HEAD -- apps/admin packages/ui

The -- separates paths. Include the app directory AND any shared packages it depends on. If you only check the app dir, a change in packages/ui would not trigger a rebuild even though it should.

For Turborepo:

npx turbo-ignore @org/admin

turbo-ignore walks the dependency graph and decides correctly.

Step 4: Ensure output directory per project matches the framework

FrameworkOutput
Next.js.next (or auto-detected)
Astrodist
Vitedist
SvelteKitbuild
Remix (Vite)build/client

In project settings, leave Output Directory empty if Vercel can auto-detect via the framework preset. Override only if your build writes elsewhere.

Step 5: Verify the workspace packages list includes every deployable app

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

Or package.json:

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

Globbing apps/* is safer than enumerating; new apps are picked up automatically.

Step 6: Test each project’s deploy independently

For each project:

  1. Make a trivial change in that app (touch a comment in a page).
  2. Push to the branch the project tracks.
  3. Verify a new deploy starts AND the live URL shows the change.

If any project does not redeploy, the Ignored Build Step is wrong. If it redeploys but the live URL does not update, output directory or domain alias is wrong.

Step 7: Set up explicit production aliases per project

Vercel → Each project → Settings → Domains:

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

Each project should own one production domain. Sharing causes the collision in cause #6. Related: canonical domain change for handling alias swaps.

Verify

  • Trivial changes in each app trigger a deploy for exactly that app’s project.
  • Each app’s production URL serves that app’s content (not 404 or wrong app).
  • turbo run build --dry from the root lists the expected build graph per filter.
  • No project has root directory ./ unless it is intentionally the only app.
  • Build logs show the expected output directory being uploaded.

Long-term prevention

  • Document the project-to-app mapping in apps/README.md or a top-level CONTRIBUTING file.
  • Adopt turbo-ignore over hand-written diff commands; it follows the dep graph correctly.
  • Pin package names in package.json name fields with a consistent @org/ scope.
  • Add a CI step that fails if a new directory appears under apps/ without a matching deploy project.
  • Avoid setting any Vercel project’s root directory to ./ in a monorepo — it should always be a subdir.
  • When adding a new app, create the deploy project FIRST, then the code, so the project never lacks a deploy target.

Common pitfalls

  • Setting Ignored Build Step to git diff --quiet HEAD^ HEAD without a path — always returns true, so deploys NEVER trigger.
  • Using --filter=web instead of --filter=@org/web when the package is scoped — Turbo silently builds nothing.
  • Copy-pasting a project from Vercel’s “duplicate” feature and forgetting to change the root directory — both projects build the same app.
  • Forgetting that apps/admin building depends on packages/ui so its ignore check must cover both — admin redeploys when it shouldn’t OR fails to redeploy when it should.
  • Manually deploying with vercel deploy from the root and being surprised that only the root’s project receives it. See vercel build failed for adjacent CLI gotchas.

FAQ

Q: Can I deploy a whole monorepo to a single Vercel project?

Possible but not recommended. You would need a next.config.js that proxies subroutes to subapps, and you lose per-app rollback/preview isolation. The standard answer is one Vercel project per deployable app.

Q: Why does Vercel charge per project? Three apps means three projects.

Yes, three projects on a paid plan equal three projects’ worth of usage if they each deploy independently. Most teams find this fine because preview isolation per app pays for itself.

Q: My monorepo uses Nx, not Turbo — does this still apply?

Same idea, different filter syntax. Use nx run web:build or nx affected --target=build --base=HEAD^ as the build command. The “Ignored Build Step” pattern is identical.

Q: Can I deploy a non-web package (a CLI, a Lambda)?

Not via Vercel — they expect a web framework. Deploy CLIs to npm or GitHub Releases and Lambdas to AWS or SST. See firebase deploy permission denied and vercel build failed for adjacent deploy-target mismatch issues.

Tags: #Troubleshooting #monorepo #turbo #Vercel #Deployment