Back to Blog
Tutorial10 min read2026-08-02

Deploying a Nuxt Static Export from a Monorepo Subdirectory

Build and deploy just the apps/marketing folder of a monorepo, pointing PandaStack at the subdirectory with rootDir so the build runs in the right context and finds the lock file.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

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.yaml

The 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 static

PandaStack 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:

[![Deploy to PandaStack](https://dashboard.pandastack.io/deploy-button.svg)](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 → uses pnpm install
  • yarn.lock → uses yarn install
  • package-lock.json → uses npm 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/html

Deploy 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/marketing

Project 2: Dashboard (SPA)

panda projects create \
  --name dashboard \
  --repo github.com/yourname/my-saas \
  --type static \
  --root-dir apps/dashboard

Project 3: API (container)

panda projects create \
  --name api \
  --repo github.com/yourname/my-saas \
  --type container \
  --root-dir apps/api

Each 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.tsgenerate.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)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also