Deployment
GitLab CI orchestrates everything. The pipeline is project-portable
(parameterized by RUN_PROJECT_ID, WIF_SERVICE_ACCOUNT, AR_HOST) so
a fresh GCP project can be onboarded by reapplying the Terraform stack
and updating those variables.
Stage diagram
Promotion model
| Service | Master push | Git tag |
|---|---|---|
| nano-server (run) | Build + deploy at 0% traffic | Promote latest to 100% |
| nano-mcp-server (run) | Build + deploy at 0% traffic | Promote latest to 100% |
| Frontend (hosting) | Preview channel per branch | Live deploy |
| Docs (hosting) | Preview channel per branch | Live deploy |
Operators promote a Cloud Run revision by pushing a tag (v3.x.y). The
CICD template's RUN_update_traffic branch fires only when
$CI_COMMIT_TAG is present.
Manual promote (if needed):
gcloud run services update-traffic nano-server --region=europe-west4 --project=cpl-gen-ai-marketing --to-latest
gcloud run services update-traffic nano-mcp-server --region=europe-west4 --project=cpl-gen-ai-marketing --to-latest
Firebase Hosting multi-site
Two sites under the same GCP project share Firestore, IAM, and billing:
| Target | Site name | URL (Firebase fallback) | Primary custom domain |
|---|---|---|---|
app | cpl-gen-ai-marketing | https://cpl-gen-ai-marketing.web.app | https://nano.cpl.ai |
docs | cpl-gen-ai-marketing-docs | https://cpl-gen-ai-marketing-docs.web.app | https://docs.nano.cpl.ai |
The MCP Cloud Run service has its own custom domain mapping:
https://mcp.nano.cpl.ai → nano-mcp-server (Cloud Run, region
europe-west1).
CI deploys each via firebase deploy --only hosting:<target> driven by
the FIREBASE_TOKEN GitLab variable (generated via firebase login:ci,
stored Masked + Protected).
One-time setup for a new project
-
Create the GCP project; enable Cloud Run, Cloud Tasks, Firestore, Firebase, Secret Manager, IAM APIs.
-
Create the
gitlab-ci@<project>.iam.gserviceaccount.comSA manually (Terraform later codifies it as adatablock). -
Run
terraform applyinterraform/bootstrap/, thenterraform/runtime/. -
Seed secret values:
gemini-api-key,mcp-private-key,mcp-google-oauth-client-id,mcp-google-oauth-client-secret. -
Create the Firebase hosting sites:
firebase hosting:sites:create cpl-gen-ai-marketing --project cpl-gen-ai-marketingfirebase hosting:sites:create cpl-gen-ai-marketing-docs --project cpl-gen-ai-marketingfirebase target:apply hosting app cpl-gen-ai-marketing --project cpl-gen-ai-marketingfirebase target:apply hosting docs cpl-gen-ai-marketing-docs --project cpl-gen-ai-marketing -
Seed the
FIREBASE_TOKENGitLab CI/CD variable (firebase login:ci). -
Configure Google OAuth client (Web type), add redirect URI
https://<mcp-url>/auth/google/callback, store client id + secret in Secret Manager. -
Push to master. First pipeline builds
ci-base, applies Terraform, builds + deploys Cloud Run services and a hosting preview channel per branch. -
Tag a release (
vX.Y.Z) to promote traffic and deploy hosting live.
DNS for custom domains
Live domains: nano.cpl.ai, docs.nano.cpl.ai, mcp.nano.cpl.ai.
DNS is managed manually in Cloud DNS (not in Terraform yet).
To add a new domain or re-bind one:
- Firebase Hosting site (or Cloud Run service) → "Add custom domain".
- Add the verification TXT record at the DNS provider.
- Wait for verification, then add the A / CNAME records the console prints. SSL provisioning is automatic (managed certificate).
- For Firebase Auth (Google sign-in) to work on the new domain, add it
to Firebase Console → Authentication → Settings → Authorized domains.
Missing this throws
Firebase: Error (auth/unauthorized-domain)at sign-in. MCP_PUBLIC_URLonnano-mcp-servermust match the public-facing URL (https://mcp.nano.cpl.ai) or every OAuthiss/audcheck fails.CLOUD_RUN_URLcontinues to point at the internal Cloud Run service URL — see ADR-0003.- CORS allowlist in
packages/server/src/index.tsmust contain the new frontend origin or browser fetches fail with CORS errors.