# Deploying Cerebrus to Cloud Run Two independent Cloud Run services, built with **Cloud Build** → **Artifact Registry**, region **us-central1**. Each app's `.env` is **baked into its image** (no Secret Manager). - `cerebrus-backend` — Express/Bun API (`oven/bun` runtime, listens on `$PORT`). - `cerebrus-frontend` — Vite SPA served by a small **Bun** server (built assets, `$PORT`). > Secrets are baked into the images, so **keep the Artifact Registry repo private** > — anyone who can pull an image can read its `.env`. ## Prerequisites - `gcloud` CLI authenticated (`gcloud auth login`) with Owner/Editor on the project. - A GCP **project id** (billing enabled). - Supabase `DATABASE_URL`, WorkOS app, and a GitHub OAuth App (see `apps/backend/README.md`). ## One-time: create the env files ```bash cp apps/backend/.env.production.example apps/backend/.env.production cp apps/frontend/.env.production.example apps/frontend/.env.production ``` Fill in the secrets now (WorkOS, Supabase, GitHub, `APP_ENCRYPTION_KEY`). Leave the URL fields at their placeholder for the first deploy — you'll set them in step 2. These files are gitignored; they only ever exist locally and inside the images. ## Deploy (two-phase, because each service needs the other's URL) ### Phase 1 — first deploy to learn the URLs ```bash ./deploy.sh ``` This enables APIs, creates the `cerebrus` Artifact Registry repo, grants Cloud Build deploy permission, builds + deploys both services, and prints their URLs, e.g.: ``` cerebrus-backend https://cerebrus-backend-abc123-uc.a.run.app cerebrus-frontend https://cerebrus-frontend-def456-uc.a.run.app ``` > **One origin.** The browser only ever talks to the **frontend** URL. The Bun > server in the frontend image reverse-proxies `/auth` and `/api` to the backend, > so the session cookie stays first-party (cross-site `*.run.app` cookies are > blocked by browsers). So every public URL you configure is the **frontend** URL — > and the frontend needs the backend URL to proxy to (`BACKEND_URL`, set once below). `deploy.sh` builds and deploys the backend, frontend, and scanner job. It reads `BACKEND_URL` from `apps/frontend/.env.production` and sets it on the frontend Cloud Run service automatically. You manage the rest of the config via the `.env.production` files (backend config is baked into its image). ### Phase 2 — wire the URLs and redeploy 1. Edit **`apps/backend/.env.production`**: - `NODE_ENV=production` - `FRONTEND_URL=` - `WORKOS_REDIRECT_URI=/auth/callback` - `GITHUB_REDIRECT_URI=/api/github/callback` - `SCAN_CALLBACK_URL=` (must be the backend, not the frontend, because the scanner calls `/internal/*`) 2. **`apps/frontend/.env.production`**: - `BACKEND_URL=` - leave `VITE_API_URL` unset (same-origin) 3. Update the external dashboards (all on the **frontend** origin): - **WorkOS**: add `/auth/callback` as a Redirect URI, and set the sign-out redirect to ``. - **GitHub OAuth App**: set the Authorization callback URL to `/api/github/callback`. 4. Redeploy. `deploy.sh` now reads `BACKEND_URL` from `apps/frontend/.env.production` and sets it on the frontend Cloud Run service automatically. It also builds and deploys the scanner Cloud Run job used when `SCAN_RUNNER=cloudrun`: ```bash ./deploy.sh ``` To skip the scanner job, run with `DEPLOY_SCANNER=0 ./deploy.sh `. ## Database Supabase Postgres is publicly reachable, so Cloud Run connects with no extra networking. Apply migrations once (from your machine, against the same `DATABASE_URL`): ```bash cd apps/backend && bun run db:migrate ``` ## Cloud Run scanner permissions (when `SCAN_RUNNER=cloudrun`) The backend Cloud Run service launches the scanner Cloud Run job. It authenticates as the service account whose key is inlined in `GOOGLE_SERVICE_ACCOUNT_JSON`. 1. In `apps/backend/.env.production`, remove any `GOOGLE_APPLICATION_CREDENTIALS` line and add the full service-account key JSON on one line: ```env GOOGLE_SERVICE_ACCOUNT_JSON={"type":"service_account","project_id":"..."} ``` 2. Grant that service account permission to run jobs: ```bash SA_EMAIL="cerebrus-job-exec@local-volt-486423-t2.iam.gserviceaccount.com" PROJECT_ID=local-volt-486423-t2 gcloud projects add-iam-policy-binding "$PROJECT_ID" \ --member="serviceAccount:${SA_EMAIL}" --role="roles/run.admin" --condition=None gcloud projects add-iam-policy-binding "$PROJECT_ID" \ --member="serviceAccount:${SA_EMAIL}" --role="roles/iam.serviceAccountUser" --condition=None ``` For local dev you can keep using `GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json` instead of inlining the JSON. ## How env is handled | | Where it's read | How it's configured | | --- | --- | --- | | Backend | runtime (`bun` auto-loads `/app/.env`) | `COPY apps/backend/.env.production ./.env` in the Dockerfile | | Frontend build | **build time** (Vite inlines `VITE_*`) | `vite build` reads `apps/frontend/.env.production` | | Frontend runtime | runtime (`process.env.BACKEND_URL`) | `deploy.sh` reads `BACKEND_URL` from `apps/frontend/.env.production` and sets it on the Cloud Run service | `PORT` is **not** set in `.env.production` — Cloud Run injects `8080`, and a real env var always wins over the file. The session cookie stays **first-party** because the browser only talks to the frontend origin (the Bun server proxies `/auth` + `/api` to the backend) — no cross-site/third-party cookies involved. ## Notes - `deploy.sh` now sets the frontend's `BACKEND_URL` Cloud Run env var automatically from `apps/frontend/.env.production` on every deploy. - Build context is the **repo root** (the bun workspace must resolve); the Dockerfiles live in each app but are built with `-f apps//Dockerfile .`. - `.gcloudignore` exists so Cloud Build keeps `.env.production` in the upload (otherwise gcloud falls back to `.gitignore`, which excludes all `.env*`). - Services are deployed `--allow-unauthenticated` (public web app). Remove that flag in the cloudbuild configs to require IAM auth.