# How to Set Up Monorepo Deployments
Monorepos are great for development — shared code, atomic cross-service changes, one place to look. They're notoriously annoying for deployment, because a naive setup rebuilds and redeploys *everything* on every push, even when you only touched one service. This guide shows how to set up clean monorepo deployments that build only what changed.
The core problem
A monorepo has multiple deployable units in one repository:
my-monorepo/
├── apps/
│ ├── web/ # Next.js frontend
│ ├── api/ # Express backend
│ └── worker/ # background worker
├── packages/
│ └── shared/ # shared TypeScript lib
├── package.json
└── pnpm-workspace.yamlDeployment platforms historically assumed one repo = one app. The keys to making a monorepo work are: per-service root directories, shared-package resolution at build time, and change detection so you don't rebuild untouched services.
Decision 1: per-service root directory
Each deployable app needs its own "root" so the platform knows where its package.json, Dockerfile, and build/start commands live. On PandaStack, you create a separate app per service and point each at its subdirectory (e.g. apps/web, apps/api, apps/worker). All three connect to the *same* GitHub repo but with different root directories.
apps/web → static site (or container)
apps/api → container app, managed DB auto-wired
apps/worker → container app (worker command)Decision 2: resolving shared packages
The trap with subdirectory roots: apps/api imports packages/shared, which lives *outside* apps/api. If the build context is only apps/api, the import fails. Two clean solutions:
A. Build from the repo root with a service-specific Dockerfile. Set the build context to the repo root so shared packages are available, and target the service in the Dockerfile:
# apps/api/Dockerfile — built with context = repo root
FROM node:20-slim
WORKDIR /repo
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages ./packages
COPY apps/api ./apps/api
RUN corepack enable && pnpm install --filter ./apps/api...
RUN pnpm --filter ./apps/api build
WORKDIR /repo/apps/api
CMD ["node", "dist/index.js"]The --filter ./apps/api... syntax (pnpm) installs and builds the API *and its workspace dependencies*, skipping unrelated apps.
B. Prebuild shared packages and reference built artifacts. Build packages/shared first, then consume its compiled output. Works but adds ordering complexity; the filtered Docker build above is usually cleaner.
Decision 3: only build what changed
This is the efficiency win. You don't want a docs typo in apps/web to redeploy apps/api. Approaches:
- Path-based triggers: configure each app to deploy only when files under its path (or its dependencies' paths) change.
- Monorepo tooling: Turborepo or Nx compute the affected projects from the dependency graph and let you build only those.
turbo run build --filter='...[origin/main]'builds only what a diff touched.
Using a tool like Turborepo also gives you remote build caching — unchanged packages are restored from cache instead of rebuilt, dramatically cutting build minutes.
Deploying a monorepo on PandaStack
- 1Push the monorepo to GitHub.
- 2For each service, create an app in the [dashboard](https://dashboard.pandastack.io), connect the same repo, and set the root directory (and build context where shared packages are involved).
- 3Set build/install/start commands per app. PandaStack auto-detects frameworks and lets you override the install command (npm/yarn/pnpm/bun) — important for pnpm/yarn workspaces.
- 4Provision a managed database once and let
DATABASE_URLauto-wire into the services that need it (e.g.api,worker) — shared cleanly across the monorepo's services. - 5Configure path-based deploys so each app rebuilds only when its code (or shared deps) changes.
A typical monorepo on PandaStack
| Service | App type | Root | Notes |
|---|---|---|---|
| web | Static site | apps/web | Any framework; built via microVM |
| api | Container app | apps/api | DATABASE_URL auto-wired |
| worker | Container app | apps/worker | Runs worker command |
| (shared) | — | packages/shared | Built as a dependency, not deployed |
Handling the database across services
A monorepo often has api and worker sharing one database. Provision it once; both apps in the same project get DATABASE_URL injected. Run migrations from a single owner (usually api as a release step, or a dedicated cronjob) so two services don't race to migrate the same schema.
Build minutes matter
Because monorepo pushes can trigger multiple builds, build minutes add up. Two levers:
- Change detection so untouched services skip builds entirely.
- Build caching (Turborepo/Nx) so even rebuilt services restore unchanged packages.
PandaStack plans include build minutes (Free 300, Pro 1000, Premium 2500). Efficient monorepo config keeps you comfortably within them; a naive "rebuild everything" setup burns through them fast.
Common pitfalls
- Shared package not found: build context too narrow — build from repo root or use workspace filters.
- Everything redeploys on every push: add path-based triggers / affected-project detection.
- Lockfile mismatch: commit a single root lockfile and use the matching package manager's workspace install.
- Migration races: designate one service as the migration owner.
References
- [Turborepo documentation](https://turbo.build/repo/docs)
- [Nx documentation](https://nx.dev/getting-started/intro)
- [pnpm workspaces](https://pnpm.io/workspaces)
- [Docker: Build context](https://docs.docker.com/build/building/context/)
Monorepo deployments come down to per-service roots, correct shared-package resolution, and building only what changed. PandaStack lets each service point at its own subdirectory of one repo, auto-wires a shared database, and gives you build minutes to work with. Set it up free at [dashboard.pandastack.io](https://dashboard.pandastack.io).