6.1 KiB
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/bunruntime, 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
gcloudCLI 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 (seeapps/backend/README.md).
One-time: create the env files
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
./deploy.sh <PROJECT_ID>
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
/authand/apito the backend, so the session cookie stays first-party (cross-site*.run.appcookies 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
- Edit
apps/backend/.env.production:NODE_ENV=productionFRONTEND_URL=<frontend url>WORKOS_REDIRECT_URI=<frontend url>/auth/callbackGITHUB_REDIRECT_URI=<frontend url>/api/github/callbackSCAN_CALLBACK_URL=<backend url>(must be the backend, not the frontend, because the scanner calls/internal/*)
apps/frontend/.env.production:BACKEND_URL=<backend url>- leave
VITE_API_URLunset (same-origin)
- Update the external dashboards (all on the frontend origin):
- WorkOS: add
<frontend url>/auth/callbackas a Redirect URI, and set the sign-out redirect to<frontend url>. - GitHub OAuth App: set the Authorization callback URL to
<frontend url>/api/github/callback.
- WorkOS: add
- Redeploy.
deploy.shnow readsBACKEND_URLfromapps/frontend/.env.productionand sets it on the frontend Cloud Run service automatically. It also builds and deploys the scanner Cloud Run job used whenSCAN_RUNNER=cloudrun:
To skip the scanner job, run with./deploy.sh <PROJECT_ID>DEPLOY_SCANNER=0 ./deploy.sh <PROJECT_ID>.
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):
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.
- In
apps/backend/.env.production, remove anyGOOGLE_APPLICATION_CREDENTIALSline and add the full service-account key JSON on one line:GOOGLE_SERVICE_ACCOUNT_JSON={"type":"service_account","project_id":"..."} - Grant that service account permission to run jobs:
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.shnow sets the frontend'sBACKEND_URLCloud Run env var automatically fromapps/frontend/.env.productionon 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/<app>/Dockerfile .. .gcloudignoreexists so Cloud Build keeps.env.productionin 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.