Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Monorepo on Netlify: Base Directory, Package Directory, and the Build That Runs Anyway

Sean

Platform Writer

Sep 09, 2026
8 min read

Deploying a monorepo on Netlify comes down to three settings that are easy to confuse: the base directory decides where the build command runs and where dependencies install, the package directory tells the platform which workspace this site belongs to, and the publish directory names the folder of built output. Get the base directory wrong and the install step misses your workspace dependencies. Get the package directory wrong and the configuration file is never read.

Monorepo on Netlify: Base Directory, Package Directory, and the Build That Runs Anyway

A monorepo deploying multiple sites from one repository is a well-trodden setup, and almost every problem with it reduces to a path being interpreted relative to a different root than you expected. Working out which root applies to which setting solves the whole category at once.

Table of contents

The three settings and what each one changes

All three are paths, all three look similar in the UI, and they do different jobs.

  • Base directory. The working directory for the build. The install command and the build command both run here, and relative paths in the build command are relative to it. Setting this to a package folder in a workspace repo is the classic mistake, because the lockfile is at the repository root and the install will not see it.
  • Package directory. Identifies which package in the monorepo this site represents. This is what lets the platform find the configuration file inside the package rather than at the repository root.
  • Publish directory. Where the built output lands. Always specify this relative to the repository root, not to the base directory, because that ambiguity is where most broken deploys come from.

The recommended arrangement for a workspace-managed monorepo is to leave the base directory at the repository root so the workspace install works properly, set the package directory to the app folder, and give the publish directory as a full path from the root.

repo-root/
  package.json          <- workspace root, lockfile lives here
  pnpm-workspace.yaml
  apps/
    marketing/
      package.json
      netlify.toml       <- config for this site
      dist/
    dashboard/
      package.json
      netlify.toml
      dist/
  packages/
    ui/
      package.json

Where the configuration file should live

A monorepo deploying two sites needs two configurations, and they cannot both be the file at the repository root. Put a configuration file inside each package directory and point the package directory setting at that package.

# apps/marketing/netlify.toml
[build]
  command = "pnpm --filter marketing build"
  publish = "apps/marketing/dist"

[[redirects]]
  from = "/old-pricing"
  to = "/pricing"
  status = 301

Note the publish path. It is written from the repository root even though the file itself sits three levels down. This is the single most common source of a deploy that builds successfully and then serves a directory listing or a 404, because the platform looked for output in a folder that does not exist.

The same applies to redirect and header files. A _redirects file inside the package directory is picked up; one at the repository root is read for the site whose package directory is the root, which in a multi-site monorepo is usually none of them.

Stopping builds that do not need to run

By default every push to the repository triggers a build of every connected site. In a monorepo with four sites that is four builds for a change that touched one of them, which burns build minutes and slows every merge.

The fix is an ignore command: a shell command that exits zero to skip the build and non-zero to proceed.

[build]
  ignore = "git diff --quiet HEAD^ HEAD -- apps/marketing packages/ui"'

That exits zero, meaning skip, when nothing under the two listed paths changed since the previous commit. Include every shared package the site depends on, or you will skip a build that needed to run, which is a far more annoying failure than an unnecessary build.

If the repository uses a build orchestrator that already understands the dependency graph, use its filter instead of hand-listing paths, because the hand-written list goes stale the first time someone adds a dependency.

# Let the tool decide whether this package is affected
npx turbo-ignore marketing

Workspace dependencies and the install step

This is where the base directory setting earns its reputation.

In a workspace repository the lockfile and the workspace manifest live at the root, and the package manager needs to run there to resolve internal dependencies. If the base directory is set to apps/marketing, the install runs inside that folder, finds a package.json referring to a workspace sibling, and fails to resolve it because there is no workspace from that vantage point.

Symptoms of getting this wrong:

  • Cannot find module errors for an internal package that clearly exists in the repository.
  • The install step reinstalling everything from scratch on every build because it never finds the cached lockfile.
  • A build that works locally from the repository root and fails in CI with no obvious difference.

Keep the base at the root, and use the package manager’s filter to build only the workspace you want.

pnpm install --frozen-lockfile
pnpm --filter marketing build

The shared package that breaks one site and not the other

The interesting monorepo failure is asymmetric. A change to a shared UI package builds fine for one app and breaks the other, usually because the two apps are on different versions of a framework or different TypeScript configurations.

Two habits that prevent most of it:

  1. Build shared packages once, in the dependency order, rather than letting each app compile the source directly. If each app transpiles the shared source with its own configuration, they can disagree about the same file.
  2. Run type checking across the whole workspace in CI, not per app. A type error introduced in a shared package should fail the pull request, not the deploy of whichever app happens to build first.

And when a deploy does break asymmetrically, check the versions before checking the code. Two apps resolving a shared peer dependency to different versions produces symptoms that look like logic bugs and are not.

The same repository on a different platform

Monorepo support differs across platforms mostly in vocabulary. The concepts are stable, which makes migrating a monorepo deployment less painful than it sounds.

Whatever the platform, the questions are the same four:

  • Where does the install command run, and does it see the lockfile?
  • Which command builds this specific package?
  • Where does the built output land, relative to which root?
  • How does the platform decide whether this push needs a build at all?

A monorepo whose answers to those four are written down in a configuration file rather than typed into a dashboard moves between platforms in about an hour. One configured entirely through UI settings takes considerably longer, because the settings are invisible until you go looking for them.

How this fits the rest of the stack

A monorepo that deploys four sites is four sets of build minutes, four sets of bandwidth and possibly a shared backend behind them, which makes the total harder to see than a single-site project. The RunxBuild hosting calculator lists the components separately so the sum is visible before the repository grows a fifth app. On RunxBuild each site builds from the same GitHub repository with its own build command and publish directory, static sites include 120GB of bandwidth, and redirects, rewrites, headers and SPA fallback are configured per site rather than shared across the repository.

Useful related references:

FAQ

What is the difference between base directory and package directory?

The base directory is where the build actually runs, so it controls where the install command executes and how relative paths resolve. The package directory identifies which workspace this site represents, which is how the platform finds a configuration file inside a package rather than at the repository root.

Why does my monorepo build fail to find an internal package?

Almost always because the base directory is set to the app folder instead of the repository root. Workspace resolution needs the lockfile and workspace manifest at the root, so the install has to run there. Keep the base at the root and use a filter flag to build one package.

How do I stop every site rebuilding on every push?

Set an ignore command that exits zero when nothing relevant changed. Either diff the paths the site depends on, including shared packages, or use a build orchestrator’s ignore helper, which reads the dependency graph and will not go stale when a dependency is added.

Should the config file be at the repository root or inside the package?

Inside the package, once you deploy more than one site from the repository. A root-level config can only describe one site. The important detail is that the publish path inside that file is still written relative to the repository root, not to the file’s own location.

Does a monorepo make deploys slower?

Only if every push builds every site. With a correct ignore command the build count matches the number of affected sites, and shared dependency caching usually makes each individual build faster than the equivalent standalone repository.

#monorepo netlify#netlify build settings#turborepo#pnpm workspace#monorepo deployment