Repository navigation
GCP_DEPLOYMENT
⚠️ Historical (2026-07). Class Mode no longer runs on GCP. The platform migrated to Azure Container Apps on 2026-07-13 (see azure-migration.md); GCP projectplp-prod-2026was decommissioned and the Cloud Run service is no longer reachable. Deployment now runs through .github/workflows/cd.yml → GHCR →az containerapp update. Kept for historical reference only.
This guide documents the live, executed deployment of PersonalLearningPro to Google Cloud Platform using Cloud Run, Secret Manager, and Cloud Build. All commands here were run and verified on 2026-05-09.
| Property | Value |
|---|---|
| URL | https://personallearningpro-wuo7arhpla-uc.a.run.app |
| Project | plp-prod-2026 |
| Region | us-central1 |
| Active revision | personallearningpro-00008-vbp |
| Health |
GET /api/health → { status: "healthy" }
|
GitHub push → Cloud Build trigger
│
▼
Docker multi-stage build
(node:20-alpine → deps → build → production)
│
▼
Artifact Registry (plp-repo)
us-central1-docker.pkg.dev/plp-prod-2026/plp-repo/personallearningpro
│
▼
Cloud Run (personallearningpro)
port 5001 | 1Gi RAM | 2 CPU | min 1 instance
│
┌────┴────┐
▼ ▼
MongoDB Secret Manager
Atlas (13 secrets)
AI stack: Google Gemini 2.0 Flash (@google/generative-ai) — all AI features including grading, study plans, and AI classroom generation.
- GCP project
plp-prod-2026with billing enabled -
gcloudCLI authenticated:gcloud auth login && gcloud config set project plp-prod-2026 - APIs already enabled:
run,cloudbuild,artifactregistry,secretmanager
| Setting | Value |
|---|---|
| Port | 5001 |
| Memory | 1Gi |
| CPU | 2 |
| Min instances | 1 (no cold starts) |
| Max instances | 10 |
| Access | Public (allUsers invoker) |
| Health check | GET /api/health |
All secrets live in plp-prod-2026. The Cloud Run compute SA (114646596478-compute@developer.gserviceaccount.com) has roles/secretmanager.secretAccessor on each.
# MongoDB Atlas connection string (must include database name)
echo -n "mongodb+srv://user:pass@cluster.mongodb.net/eduai?retryWrites=true&w=majority&appName=Cluster0" \
| gcloud secrets versions add MONGODB_URL --project=plp-prod-2026 --data-file=-
# Firebase service account (raw JSON from Firebase Console → Service Accounts)
cat your-service-account.json \
| gcloud secrets versions add FIREBASE_SERVICE_ACCOUNT_JSON --project=plp-prod-2026 --data-file=-
# Gemini API key (Google AI Studio → API Keys)
echo -n "AIza..." \
| gcloud secrets versions add GOOGLE_API_KEY --project=plp-prod-2026 --data-file=-
# Random secrets (generate with: openssl rand -hex 32)
openssl rand -hex 32 | gcloud secrets versions add SESSION_SECRET --project=plp-prod-2026 --data-file=-
openssl rand -hex 32 | gcloud secrets versions add JWT_SECRET --project=plp-prod-2026 --data-file=-
openssl rand -hex 32 | gcloud secrets versions add REFRESH_SECRET --project=plp-prod-2026 --data-file=-# Stripe (billing — returns 503 until a real sk_live_/sk_test_ key is set)
echo -n "sk_live_..." | gcloud secrets versions add STRIPE_SECRET_KEY --project=plp-prod-2026 --data-file=-
# OpenAI (optional fallback for AI features — Gemini is primary)
echo -n "sk-..." | gcloud secrets versions add OPENAI_API_KEY --project=plp-prod-2026 --data-file=-
# Daily.co (video calls)
echo -n "..." | gcloud secrets versions add DAILY_API_KEY --project=plp-prod-2026 --data-file=-
# SMTP (email / invites)
echo -n "smtp.yourprovider.com" | gcloud secrets versions add SMTP_HOST --project=plp-prod-2026 --data-file=-
echo -n "user@example.com" | gcloud secrets versions add SMTP_USER --project=plp-prod-2026 --data-file=-
echo -n "yourpassword" | gcloud secrets versions add SMTP_PASS --project=plp-prod-2026 --data-file=-
# IniClaw gateway auth (internal microservice bridge)
openssl rand -hex 32 | gcloud secrets versions add BRIDGE_SECRET --project=plp-prod-2026 --data-file=-gcloud secrets add-iam-policy-binding SECRET_NAME \
--project=plp-prod-2026 \
--member="serviceAccount:114646596478-compute@developer.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"These are set directly on the service (not via Secret Manager):
| Variable | Value |
|---|---|
NODE_ENV |
production |
DNS_IPV4_FIRST |
true |
CORS_ORIGIN |
https://inmodel.in,https://personallearningpro-wuo7arhpla-uc.a.run.app,https://personallearningpro-114646596478.us-central1.run.app |
APP_URL |
https://inmodel.in |
FIREBASE_PROJECT_ID |
presonallp |
CORS note: The app code also allows
*.run.appand*.inmodel.inhostnames in production, so adding new Cloud Run revisions with different URL formats will work automatically.
Set these Substitution Variables in the Cloud Build trigger UI:
| Variable | Value |
|---|---|
_VITE_FIREBASE_API_KEY |
YOUR_FIREBASE_API_KEY |
_VITE_FIREBASE_APP_ID |
YOUR_FIREBASE_APP_ID |
_VITE_FIREBASE_MESSAGING_SENDER_ID |
YOUR_FIREBASE_SENDER_ID |
_VITE_FIREBASE_MEASUREMENT_ID |
`` |
_VITE_FIREBASE_PROJECT_ID is automatically set to $PROJECT_ID in cloudbuild.yaml.
cd PersonalLearningPro
SHORT_SHA=$(git rev-parse --short HEAD)
gcloud builds submit \
--project=your-project-id \
--config=cloudbuild.yaml \
--substitutions="SHORT_SHA=${SHORT_SHA},\
_VITE_FIREBASE_API_KEY=YOUR_FIREBASE_API_KEY,\
_VITE_FIREBASE_APP_ID=YOUR_FIREBASE_APP_ID,\
_VITE_FIREBASE_MESSAGING_SENDER_ID=YOUR_FIREBASE_SENDER_ID"Or use the npm shortcut (requires the trigger substitution variables to be set):
npm run deploy:gcp- Build — Multi-stage Docker build (node:20-alpine), injects Firebase keys as build-args for Vite
-
Push — Tags image with
${SHORT_SHA}and:latest, pushes both to Artifact Registry -
Deploy —
gcloud run deploywith--platform managed --port 5001 --allow-unauthenticated
Cloud Run preserves existing secrets, env vars, memory, and CPU settings across deployments triggered by Cloud Build (only the image changes).
To deploy an already-built image directly:
gcloud run deploy personallearningpro \
--project=plp-prod-2026 \
--image=us-central1-docker.pkg.dev/plp-prod-2026/plp-repo/personallearningpro:latest \
--region=us-central1 \
--platform=managed \
--port=5001 \
--memory=1Gi \
--cpu=2 \
--min-instances=1 \
--max-instances=10 \
--allow-unauthenticated \
--set-env-vars="NODE_ENV=production,DNS_IPV4_FIRST=true,CORS_ORIGIN=https://inmodel.in,APP_URL=https://inmodel.in,FIREBASE_PROJECT_ID=presonallp" \
--set-secrets="MONGODB_URL=MONGODB_URL:latest,GOOGLE_API_KEY=GOOGLE_API_KEY:latest,FIREBASE_SERVICE_ACCOUNT_JSON=FIREBASE_SERVICE_ACCOUNT_JSON:latest,SESSION_SECRET=SESSION_SECRET:latest,JWT_SECRET=JWT_SECRET:latest,REFRESH_SECRET=REFRESH_SECRET:latest,STRIPE_SECRET_KEY=STRIPE_SECRET_KEY:latest,OPENAI_API_KEY=OPENAI_API_KEY:latest,DAILY_API_KEY=DAILY_API_KEY:latest,SMTP_HOST=SMTP_HOST:latest,SMTP_USER=SMTP_USER:latest,SMTP_PASS=SMTP_PASS:latest,BRIDGE_SECRET=BRIDGE_SECRET:latest"# Example: update SMTP password
echo -n "new-password" | gcloud secrets versions add SMTP_PASS --project=plp-prod-2026 --data-file=-
# Cloud Run will pick up the new version on the next cold start.
# To force an immediate rollout:
gcloud run deploy personallearningpro \
--project=plp-prod-2026 \
--region=us-central1 \
--image=us-central1-docker.pkg.dev/plp-prod-2026/plp-repo/personallearningpro:latest- Add a real key to Secret Manager:
echo -n "sk_live_REAL_KEY" | gcloud secrets versions add STRIPE_SECRET_KEY --project=plp-prod-2026 --data-file=-
- Trigger a new deploy (the billing routes auto-activate when the key starts with
sk_live_orsk_test_).
To put Firebase Hosting in front of Cloud Run (for CDN caching and custom domain):
// firebase.json
"rewrites": [
{
"source": "**",
"run": {
"serviceId": "personallearningpro",
"region": "us-central1"
}
}
]firebase deploy --only hostingThe CORS middleware rejects origins not in the allowlist. Add your domain:
gcloud run services update personallearningpro \
--region=us-central1 \
--project=plp-prod-2026 \
"--update-env-vars=^;^CORS_ORIGIN=https://inmodel.in,https://your-new-domain.com"Use
^;^prefix so gcloud uses;as the delimiter — the value itself contains commas.
Check Cloud Run logs:
gcloud logging read \
'resource.type="cloud_run_revision" AND resource.labels.service_name="personallearningpro"' \
--project=plp-prod-2026 --limit=30 --order=asc \
--format="value(timestamp,textPayload)"Common causes:
-
SESSION_SECRET environment variable is required→ secret not mounted or no version exists -
bad auth: authentication failed→ MongoDB URL missing database name (add/eduai?retryWrites=true&w=majority) -
Gemini service not initialized→GOOGLE_API_KEYsecret is empty
curl https://personallearningpro-wuo7arhpla-uc.a.run.app/api/health
# Expected: { "status": "healthy", "databases": { "mongodb": { "connected": true } } }