Skip to main content

Deploy a monorepo

One repository, several deployable apps, each with its own file and its own lifecycle. Nothing is ever discovered — you list what deploys.

The layout

shop/
├── pagifier.toml ← root manifest: lists the projects
├── frontend/
│ ├── pagifier.toml
│ └── src/…
└── api/
├── pagifier.toml
└── src/…
pagifier.toml (root)
version = 1

projects = [
"frontend/pagifier.toml",
"api/pagifier.toml",
]

The root manifest is the explicit inventory: a project not listed here does not exist as far as Pagifier is concerned.

The two projects

frontend/pagifier.toml
version = 1
name = "shop-frontend"
template = "vite-static"
environment = "prod"
ingress = "shop.company.com"

# Same-host path routing to the API service:
[[routes]]
path = "/api"
service = "shop-api"
port = 80
api/pagifier.toml
version = 1
name = "shop-api"
template = "nestjs"
environment = "prod"
ingress = "api.shop.company.com"

[secrets]
provider = "aws"

[secrets.keys]
DATABASE_URL = "prod/shop/database-url"

Each project is completely independent: its own releases, its own rollbacks, its own canary progression. The frontend's routes block proxies shop.company.com/api/* to the API service inside the cluster — no CORS needed for same-host calls.

Deploying only what changed

Each project deploys with its own upload — so CI uses path filters:

.github/workflows/deploy.yml
name: deploy
on:
push:
branches: [main]

jobs:
changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
api: ${{ steps.filter.outputs.api }}
steps:
- uses: actions/checkout@v7
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
frontend: ['frontend/**']
api: ['api/**']

frontend:
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: company/pagifier-deploy@v1
with:
config: frontend/pagifier.toml
workdir: frontend
environment: prod
token: ${{ secrets.PAGIFIER_TOKEN }}

api:
needs: changes
if: needs.changes.outputs.api == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: company/pagifier-deploy@v1
with:
config: api/pagifier.toml
workdir: api
environment: prod
token: ${{ secrets.PAGIFIER_TOKEN }}

A commit touching only api/ deploys only the API; the frontend's running release is untouched.

Shared code?

Zip whatever the build needs. If both apps build from the repo root (shared packages, workspaces), keep workdir: . and set each project's build.workDir:

frontend/pagifier.toml — workspace variant
[build]
enabled = true
workDir = "frontend" # where the commands run
commands = ["npm ci", "npm run build -w frontend"]

Tips that save monorepo pain

  • Independent environments too: deploy api to staging while frontend rides prod — environment is per-upload, not per-repo.
  • Preview both per PR: run both deploy jobs with environment: preview; each gets its own auto-generated URL.
  • Identical artifact = no rebuild: if a commit touches only frontend/, an accidental api upload is nearly free — Pagifier fingerprints build inputs and reuses the existing image.