Monorepos are great for keeping multiple apps in one repository — shared UI components, a backend API, a marketing site, and a dashboard all version-controlled together. The problem comes at deploy time. Most platforms assume the repo root is the app root. When your Nuxt app lives in apps/marketing, the build fails because the platform runs npm install in the root directory where there is no package.json, or finds the wrong lock file and installs the wrong dependencies.
PandaStack supports monorepo deploys via the rootDir field. Point it at the subdirectory, and the build runs there instead of at the repo root. Lock files, environment files, and build commands all resolve relative to that directory.
Monorepo structure
A typical monorepo for a SaaS product looks like this:
my-saas/
├── apps/
│ ├── marketing/ ← Nuxt static site (what we're deploying)
│ │ ├── package.json
│ │ ├── nuxt.config.ts
│ │ └── .output/
│ ├── dashboard/ ← Vue SPA
│ └── api/ ← Node.js backend
├── packages/
│ ├── ui/ ← Shared components
│ └── utils/
├── package.json ← Root workspace config (yarn/pnpm/npm workspaces)
└── pnpm-lock.yamlThe root package.json defines workspaces. Each app in apps/ has its own package.json and can be built independently. The lock file is at the root because that is where the package manager resolves dependencies for the entire monorepo.
If you deploy the repo without specifying a subdirectory, the platform sees the root package.json, which has no build script, and the deploy fails.
Deploy the marketing site with rootDir
Create a pandastack.json at the repo root:
{
"type": "static",
"name": "marketing-site",
"rootDir": "apps/marketing",
"buildCommand": "npm run generate",
"outputDir": ".output/public"
}The rootDir field tells PandaStack to cd into apps/marketing before running the install and build commands. Dependencies are installed relative to that directory, but the lock file is resolved from the repo root (where it actually lives).
Push this config to the repo and deploy via the dashboard, CLI, or API:
panda projects create \
--name marketing-site \
--repo github.com/yourname/my-saas \
--branch main \
--type staticPandaStack reads pandastack.json, clones the repo, runs npm install in apps/marketing (but reads pnpm-lock.yaml from the root), executes npm run generate, and uploads .output/public to the CDN.
Use the deploy button for monorepo subdirectories
Add a README badge in apps/marketing/README.md:
[](https://dashboard.pandastack.io/deploy?repo=yourname/my-saas&rootDir=apps/marketing)The rootDir query parameter works the same way as the pandastack.json field. When someone clicks the button, PandaStack pre-fills the deploy form with the subdirectory path and the build runs in the right context.
You can also use root-directory (with a hyphen) as an alias. Both forms work identically.
Handle workspace dependencies
If your Nuxt app imports shared packages from packages/ui, the workspace setup must be preserved during the build. PandaStack installs dependencies based on the lock file format:
pnpm-lock.yaml→ usespnpm installyarn.lock→ usesyarn installpackage-lock.json→ usesnpm install
For pnpm and Yarn workspaces, the lock file at the root links the subdirectory dependencies correctly. When the build runs in apps/marketing, pnpm resolves @my-saas/ui to ../../packages/ui because the workspace config in the root package.json tells it to.
If you use npm workspaces (npm v7+), the same logic applies. Older npm versions do not support workspaces and will fail — upgrade to npm 7+ or switch to pnpm.
Override the install command for Bun or Turborepo
By default, PandaStack auto-detects the package manager from the lock file. If your monorepo uses Bun or a meta-runner like Turborepo, you need to set a custom install command.
In pandastack.json:
{
"type": "static",
"rootDir": "apps/marketing",
"buildCommand": "turbo run build --filter=marketing",
"outputDir": ".output/public"
}For Turborepo, the build command targets just the marketing package. The install step still runs pnpm install (or npm install) at the root to set up the workspace, then Turborepo handles the selective build.
If you use Bun and the root has a bun.lockb, the buildpack should auto-detect it and use bun install. If not, file an issue or use a custom Dockerfile:
FROM oven/bun:latest AS builder
WORKDIR /app
COPY package.json bun.lockb ./
COPY apps/marketing apps/marketing
COPY packages packages
RUN bun install
RUN cd apps/marketing && bun run generate
FROM nginx:alpine
COPY --from=builder /app/apps/marketing/.output/public /usr/share/nginx/htmlDeploy this as a container app instead of a static site, and point PandaStack at the Dockerfile.
Environment variables in a subdirectory build
Set environment variables in pandastack.json or via the CLI. They are available during the build, even when the build runs in a subdirectory.
{
"type": "static",
"rootDir": "apps/marketing",
"buildCommand": "npm run generate",
"outputDir": ".output/public",
"env": [
{ "key": "NUXT_PUBLIC_API_URL", "description": "Backend API URL" }
]
}Nuxt 3 reads variables prefixed with NUXT_PUBLIC_ and makes them available at useRuntimeConfig().public.apiUrl. When PandaStack runs the build, it sets NUXT_PUBLIC_API_URL in the shell environment, and Nuxt bakes it into the static output at build time.
For secrets (like API keys used in server-side rendering), use non-public variables (NUXT_SECRET_KEY). These are not exposed to the client bundle.
Deploy multiple apps from the same monorepo
To deploy the dashboard and marketing site separately, create two PandaStack projects pointing at different subdirectories.
Project 1: Marketing site
panda projects create \
--name marketing \
--repo github.com/yourname/my-saas \
--type static \
--root-dir apps/marketingProject 2: Dashboard (SPA)
panda projects create \
--name dashboard \
--repo github.com/yourname/my-saas \
--type static \
--root-dir apps/dashboardProject 3: API (container)
panda projects create \
--name api \
--repo github.com/yourname/my-saas \
--type container \
--root-dir apps/apiEach project builds and deploys independently. A commit to main can trigger all three via webhooks or CI, or you can deploy selectively based on which files changed (using GitHub Actions path filters, for example).
Debugging monorepo build failures
Lock file not found: The build tried to install dependencies but could not find the lock file. Make sure the lock file is at the repo root, not inside the subdirectory. PandaStack walks up from rootDir to the repo root looking for it.
Workspace package not found: The build failed with "Cannot find module '@my-saas/ui'". The workspace config in the root package.json is missing or malformed. Verify "workspaces": ["apps/*", "packages/*"] is set correctly.
Build command failed: npm run generate does not exist in apps/marketing/package.json. Check the script name. Nuxt 2 uses nuxt generate; Nuxt 3 uses nuxt generate or a custom script that calls it.
Wrong output directory: The build succeeded but PandaStack cannot find the static files. Nuxt 3's static output goes to .output/public by default. Nuxt 2 uses dist. Check nuxt.config.ts → generate.dir or nitro.output.publicDir and match the outputDir field in your config.
When to use rootDir vs. separate repos
Monorepos work well when the apps share code and are deployed by the same team. If you have ten apps with no shared dependencies, splitting them into separate repos simplifies deploys — no need for subdirectory config or workspace setup.
Use monorepos when:
- Apps share a UI library or utility package
- Backend and frontend versions must stay in sync
- You want one CI pipeline to run tests across all apps
Use separate repos when:
- Apps are maintained by different teams with different release cycles
- Builds are independent and do not share dependencies
- The monorepo is growing too large and slowing down CI
PandaStack supports both models. The rootDir field makes monorepos work without forcing you to split them up.
References
- [Nuxt 3 deployment guide](https://nuxt.com/docs/getting-started/deployment)
- [pnpm workspaces documentation](https://pnpm.io/workspaces)
- [PandaStack monorepo deployment docs](https://docs.pandastack.io)