Back to Blog
Tutorial10 min read2026-07-04

How to Deploy a Monorepo with Multiple Services

A guide to deploying multiple services from a single monorepo — root directories, build context, shared packages, selective rebuilds, and keeping a web app, API, and worker in sync.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

# How to Deploy a Monorepo with Multiple Services

Monorepos are popular for good reasons: shared code, atomic cross-service commits, one place to look. But deploying multiple independent services out of one repository introduces its own challenges. This guide covers the patterns that make it work.

What a typical monorepo looks like

A full-stack monorepo often has several deployable units side by side:

my-monorepo/
├── apps/
│   ├── web/          # React/Vite frontend (static)
│   ├── api/          # Express/FastAPI backend (container)
│   └── worker/       # Background job processor (container)
├── packages/
│   └── shared/       # Shared types/utilities
├── package.json      # Workspace root
└── pnpm-workspace.yaml

Each of web, api, and worker is a separate deployment, but they all share the same Git history and possibly the shared package.

Challenge 1: Telling the platform where each service lives

The key concept is the root directory (sometimes called build context or working directory). When you create a service, you tell the platform which subdirectory to treat as that service's root.

ServiceRoot directoryType
Frontendapps/webStatic site
APIapps/apiContainer
Workerapps/workerContainer

Each service is configured independently — its own build command, start command, environment variables, and scaling. They just happen to come from the same repo.

Challenge 2: Shared packages and build context

The tricky part: if apps/api imports from packages/shared, building with a root directory of apps/api may not include packages/shared in the build context. There are two common solutions.

Option A — Build from the repo root with a workspace-aware command:

# Install everything, build only what's needed
pnpm install --frozen-lockfile
pnpm --filter ./apps/api... build

The ... suffix tells pnpm to also build the API's dependencies (like shared).

Option B — Use a Dockerfile that copies the workspace:

FROM node:20-slim AS build
WORKDIR /repo
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
COPY packages ./packages
COPY apps/api ./apps/api
RUN corepack enable && pnpm install --frozen-lockfile
RUN pnpm --filter ./apps/api... build

FROM node:20-slim
WORKDIR /app
COPY --from=build /repo/apps/api/dist ./dist
COPY --from=build /repo/node_modules ./node_modules
CMD ["node", "dist/index.js"]

Challenge 3: Avoiding unnecessary rebuilds

When you push a one-line CSS change to the frontend, you don't want the API and worker to rebuild and redeploy. Selective deploys save build minutes and reduce risk.

The standard approach is watch paths (or build filters): each service only rebuilds when files under its directory (or its shared dependencies) change. If your platform supports it, configure each service to watch apps/web/ plus packages/shared/, etc.

If the platform doesn't filter for you, tools like [Turborepo](https://turbo.build/repo) or [Nx](https://nx.dev/) can compute the affected projects from a Git diff:

# Only the projects affected since the last deploy
npx turbo run build --filter='...[HEAD^1]'

Challenge 4: Keeping services in sync

The upside of a monorepo is atomic changes — update the API's response shape and the frontend that consumes it in the same commit. The risk is deploying them at different speeds. For breaking changes, deploy the *backward-compatible* side first (e.g. the API accepting both old and new shapes), then the consumer, then remove the old path. This is the same discipline as zero-downtime database migrations.

Deploying a monorepo on PandaStack

PandaStack supports multiple services from one repository: you create each service and point it at its subdirectory as the root. A static frontend, a containerized API, and a worker can all live in one repo and deploy independently, each with its own build command, start command, environment variables, and scaling settings.

Because PandaStack auto-detects framework, build, and start commands, a Vite frontend in apps/web and a Node API in apps/api are recognized appropriately, and you can override the install command (npm/yarn/pnpm/bun) when you're using workspaces. Each service deploys on its own — pushing a change builds and ships the relevant service, with live build logs per service so you can watch exactly what's compiling.

For shared packages, the Dockerfile-from-root pattern above is the most reliable, since it gives you explicit control over what's in the build context.

A practical checklist

  • ✅ Each deployable has a clearly defined root directory
  • ✅ Shared packages are included in the build context
  • ✅ Build commands use workspace filters to build only what's needed
  • ✅ Each service has independent env vars and scaling
  • ✅ Breaking changes deploy in a backward-compatible order
  • ✅ Watch paths prevent unrelated rebuilds (where supported)

References

  • [pnpm workspaces](https://pnpm.io/workspaces)
  • [Turborepo documentation](https://turbo.build/repo/docs)
  • [Nx monorepo documentation](https://nx.dev/concepts/mental-model)
  • [Docker build context](https://docs.docker.com/build/building/context/)

Deploy your whole monorepo — frontend, API, and workers — on PandaStack. Start with the free tier at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also