Monorepos group related projects into one repository: a frontend in apps/web, a backend in apps/api, shared utilities in packages/common. This structure works well for teams that want atomic cross-project commits and centralized dependency management, but most deploy platforms assume one repo equals one deployable app and break when you point them at a monorepo root.
The failure mode is predictable: the platform detects multiple package.json files, picks the wrong one, runs npm install in the repo root instead of the app directory, then tries to start a server that does not exist. The build succeeds but the app never boots, and the health check times out.
PandaStack's rootDir parameter solves this by telling the build system which subdirectory contains the deployable app. The build runs inside that directory, dependencies install correctly, and the start command executes the right entry point.
Why monorepos break the default deploy path
Most platforms run the build in the repository root. They look for package.json in the top-level directory, run npm install, then execute the start command. This works perfectly for single-app repos but fails for monorepos where package.json at the root is a workspace config, not a deployable app.
For example, a Turborepo structure might look like:
my-monorepo/
├── package.json (workspace root)
├── apps/
│ ├── web/ (Next.js frontend)
│ │ └── package.json
│ └── api/ (Express backend)
│ └── package.json
└── packages/
└── shared/
└── package.jsonRunning npm install in the root sets up the workspace but does not install dependencies for apps/api. Running npm start errors because there is no start script in the root package.json. The platform needs to cd apps/api && npm install && npm start, which requires knowing the subdirectory path.
Set rootDir in pandastack.json to deploy a subdirectory
The rootDir field in pandastack.json tells the platform which directory to treat as the app root. Place the file in the repository root, then specify the relative path to the deployable app:
{
"type": "container",
"name": "express-api",
"rootDir": "apps/api",
"startCommand": "node index.js"
}When the build runs, the system:
- 1Clones the entire repository
- 2Changes directory to
apps/api - 3Runs
npm install(or the detected package manager) - 4Executes the start command from inside
apps/api
The API boots correctly because the build context is the subdirectory, not the root.
If your monorepo uses Yarn workspaces or pnpm, dependencies might hoist to the root node_modules. The build still works because the package manager resolves modules relative to the workspace root, and the platform preserves the full repository structure during the build.
Deploy via the API with rootDir in the request payload
If you are automating deploys from CI or a script, pass rootDir in the API request instead of committing pandastack.json:
curl -X POST https://api.pandastack.io/v1/projects \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"slug": "container",
"name": "express-api",
"repositoryName": "yourname/my-monorepo",
"branch": "main",
"autoDeploy": true,
"rootDir": "apps/api",
"startCommand": "node index.js"
}'The platform clones the repo, builds from apps/api, and deploys the container. This approach is useful when multiple apps in the monorepo deploy to different projects and you do not want a single pandastack.json that hardcodes one path.
Add rootDir to the deploy button for template repos
If your monorepo is a starter template and you want users to deploy the API with one click, encode rootDir in the deploy button URL:
[](https://dashboard.pandastack.io/deploy?repo=yourname/my-monorepo&type=container&rootDir=apps/api&name=express-api)Query parameters:
repo:owner/repotype:container(orstaticfor a frontend subdirectory)rootDir: relative path to the app directoryname: project name on the platform
When someone clicks the badge, the deploy screen pre-fills the subdirectory path. The user can override it if needed, but the default is correct, so most people just click Deploy.
Handle shared dependencies in packages/ directories
If apps/api depends on packages/shared, the imports resolve correctly as long as the workspace is configured properly. For example, with pnpm workspaces:
{
"name": "express-api",
"dependencies": {
"shared": "workspace:*"
}
}The package manager symlinks packages/shared into apps/api/node_modules, and the Node.js module resolver finds it when the app runs require('shared') or import { foo } from 'shared'.
PandaStack preserves the full repository structure during the build, so workspace links stay intact. The only requirement is that your package.json and workspace config are valid — the platform does not modify the dependency graph.
Set different start commands per subdirectory
Monorepo apps often have different start scripts. The frontend might run next start, the API might run node src/server.js, and a worker might run node worker.js. You cannot set a single start command in the root package.json that works for all of them.
Each project gets its own startCommand in pandastack.json or the API request:
{
"type": "container",
"name": "express-api",
"rootDir": "apps/api",
"startCommand": "node src/server.js"
}If the subdirectory's package.json has a start script, the platform auto-detects it and you do not need to override startCommand. But if the script is named differently or you want to pass command-line flags, specify it explicitly.
Debug "Cannot find module" errors after deploy
If your app builds successfully but crashes at runtime with Cannot find module 'some-package', the dependency is missing from apps/api/package.json even though it works locally. This happens when a package is installed in the root node_modules and Node resolves it via hoisting, but the dependency is not declared in the subdirectory's package.json.
The fix is to add the dependency to apps/api/package.json:
cd apps/api
npm install some-packageCommit the change and redeploy. The package installs into the subdirectory's dependencies, and the app boots correctly.
Deploy multiple apps from the same monorepo
If you want to deploy both apps/web and apps/api, create two separate projects on the platform, each with a different rootDir:
Project 1 (frontend):
curl -X POST https://api.pandastack.io/v1/projects \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"slug": "static",
"name": "web",
"repositoryName": "yourname/my-monorepo",
"branch": "main",
"rootDir": "apps/web",
"buildCommand": "npm run build",
"outputDir": "dist"
}'Project 2 (backend):
curl -X POST https://api.pandastack.io/v1/projects \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"slug": "container",
"name": "api",
"repositoryName": "yourname/my-monorepo",
"branch": "main",
"rootDir": "apps/api",
"startCommand": "node src/server.js"
}'Both projects watch the same repository. When you push a commit, PandaStack detects which apps changed by diffing the file paths and only rebuilds the affected ones. If you edit a file in apps/api, only the API redeploys; the frontend stays untouched.
Set environment variables per app
Each project gets its own set of environment variables. The frontend might need VITE_API_URL, the backend might need DATABASE_URL. You configure them separately in each project's settings or via the API:
panda projects env <api-project-id> --set DATABASE_URL=postgres://...
panda projects env <web-project-id> --set VITE_API_URL=https://api.example.comThe variables are scoped to the project, so there is no risk of the frontend accidentally inheriting the database credentials or the backend getting the wrong API URL.
References
- [Express deployment best practices](https://expressjs.com/en/advanced/best-practice-performance.html)
- [PandaStack monorepo guide](https://docs.pandastack.io/projects/)
- [Turborepo documentation](https://turbo.build/repo/docs)