cefense/DEPLOYMENT.md
2026-08-18 19:38:47 +05:30

6.1 KiB

Deploying Cerebrus to Cloud Run

Two independent Cloud Run services, built with Cloud BuildArtifact 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

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 /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=<frontend url>
    • WORKOS_REDIRECT_URI=<frontend url>/auth/callback
    • GITHUB_REDIRECT_URI=<frontend url>/api/github/callback
    • SCAN_CALLBACK_URL=<backend url> (must be the backend, not the frontend, because the scanner calls /internal/*)
  2. apps/frontend/.env.production:
    • BACKEND_URL=<backend url>
    • leave VITE_API_URL unset (same-origin)
  3. Update the external dashboards (all on the frontend origin):
    • WorkOS: add <frontend url>/auth/callback as 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.
  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:
    ./deploy.sh <PROJECT_ID>
    
    To skip the scanner job, run with 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.

  1. In apps/backend/.env.production, remove any GOOGLE_APPLICATION_CREDENTIALS line and add the full service-account key JSON on one line:
    GOOGLE_SERVICE_ACCOUNT_JSON={"type":"service_account","project_id":"..."}
    
  2. 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.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/<app>/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.