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

135 lines
6.1 KiB
Markdown

# 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 <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`:
```bash
./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`):
```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/<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.