Back to Blog
Guide7 min read2026-05-01

Monorepo Deployment: How to Deploy Multiple Services from One Repo

Learn how to structure a monorepo and set up path-filtered CI/CD pipelines that deploy only the services affected by each commit.

What Is a Monorepo?

A monorepo stores multiple projects — frontend, backend, shared libraries, infrastructure config — in a single Git repository. Instead of a separate repo per service (polyrepo), everything lives together. Google, Meta, and Airbnb famously operate at massive scale with monorepos.

The primary challenge for deployment is selectivity: when you change only the frontend, you do not want to rebuild and redeploy the backend. Getting this right is what separates a well-architected monorepo from a slow, expensive mess.

Monorepo Directory Structure

my-monorepo/
├── apps/
│   ├── web/          ← React frontend
│   ├── api/          ← Node.js backend
│   └── worker/       ← Background job processor
├── packages/
│   ├── ui/           ← Shared React components
│   ├── utils/        ← Shared utilities
│   └── types/        ← Shared TypeScript types
├── panda.web.yaml    ← PandaStack config for frontend
├── panda.api.yaml    ← PandaStack config for backend
└── package.json      ← Workspace root

Using a package manager with workspace support (npm workspaces, pnpm workspaces, or Yarn workspaces) lets you install all dependencies from the root and share code between packages without publishing to npm.

# package.json (root)
{
  "name": "my-monorepo",
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}

Path-Filtered CI: Deploy Only What Changed

The key to efficient monorepo CI/CD is path filtering — running a job only when files in a specific directory change:

# .github/workflows/deploy-web.yml
name: Deploy Web Frontend

on:
  push:
    branches: [main]
    paths:
      - 'apps/web/**'
      - 'packages/ui/**'      # also rebuild web if shared UI changes
      - 'packages/types/**'

jobs:
  deploy-web:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - name: Install dependencies
        run: npm ci
      - name: Build web app
        run: npm run build --workspace=apps/web
      - name: Deploy to PandaStack
        run: |
          npm install -g @pandastack/cli
          panda login --token ${{ secrets.PANDA_TOKEN }}
          panda deploy --config panda.web.yaml --branch main
# .github/workflows/deploy-api.yml
name: Deploy API Backend

on:
  push:
    branches: [main]
    paths:
      - 'apps/api/**'
      - 'packages/utils/**'
      - 'packages/types/**'

jobs:
  deploy-api:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm test --workspace=apps/api
      - name: Deploy to PandaStack
        run: |
          npm install -g @pandastack/cli
          panda login --token ${{ secrets.PANDA_TOKEN }}
          panda deploy --config panda.api.yaml --branch main

PandaStack Configuration per Service

Each service gets its own PandaStack configuration file:

# panda.api.yaml
name: my-app-api
type: container
branch: main
build:
  dockerfile: apps/api/Dockerfile
  context: .
env:
  NODE_ENV: production
  PORT: "3000"
resources:
  cpu: 500m
  memory: 512Mi
# panda.web.yaml
name: my-app-web
type: static
branch: main
build:
  command: npm run build --workspace=apps/web
  output: apps/web/dist

Affected-Package Detection

For more sophisticated change detection — where you want to rebuild services that depend on changed packages — use a tool like turbo or write a custom script:

#!/bin/bash
# scripts/affected.sh
# Determine which apps changed since last deploy

CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD)

deploy_web=false
deploy_api=false

echo "$CHANGED_FILES" | grep -qE '^(apps/web|packages/ui)/' && deploy_web=true
echo "$CHANGED_FILES" | grep -qE '^(apps/api|packages/utils)/' && deploy_api=true

echo "deploy_web=$deploy_web" >> $GITHUB_OUTPUT
echo "deploy_api=$deploy_api" >> $GITHUB_OUTPUT
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      deploy_web: ${{ steps.affected.outputs.deploy_web }}
      deploy_api: ${{ steps.affected.outputs.deploy_api }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2
      - id: affected
        run: bash scripts/affected.sh

Shared Libraries and Versioning

When a shared package changes, all services that depend on it should be rebuilt. Document dependencies explicitly:

# .github/workflows/rebuild-all.yml
name: Rebuild All Services

on:
  push:
    branches: [main]
    paths:
      - 'packages/types/**'   # Core types — everything rebuilds

jobs:
  deploy-all:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build   # Build all workspaces
      - run: |
          npm install -g @pandastack/cli
          panda login --token ${{ secrets.PANDA_TOKEN }}
          panda deploy --config panda.web.yaml --branch main
          panda deploy --config panda.api.yaml --branch main

With path filtering in place, a monorepo on PandaStack gives you the collaboration benefits of a unified codebase with the deployment efficiency of independent services — each deploying only when its code actually changes.

Ready to deploy?

Start free on PandaStack — no credit card required.

Start free on PandaStack

More in Guide

Browse all Guide articles →

See also