deploy: Dockerfile + Google Cloud Run recipe

Add a CGO-free static Dockerfile (distroless) and a one-shot Cloud Run
deploy recipe (deploy/gcp-cloudrun.sh + README): single-instance hub on
Cloud Run, metadata in Cloud SQL Postgres (the new `database` backend),
blobs/journals in a GCS bucket, config + DB password in Secret Manager,
least-privilege runtime service account.

Pinned to max-instances=1 (the current build assumes one writer). Verified
end to end against a live deployment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
This commit is contained in:
Snow Lee
2026-07-10 08:51:55 -07:00
co-authored by Claude Opus 4.8
parent 62175e29a9
commit a9cc43354d
4 changed files with 231 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
# Deploy BearDrive to Google Cloud (Cloud Run)
Single-instance hub on **Cloud Run**, metadata in **Cloud SQL Postgres**,
blobs/journals in a **GCS bucket**. Matches Phase 0 of the managed PRD.
> The current build is single-process: Cloud Run is pinned to
> `max-instances=1` because the in-memory caches assume one writer. Do **not**
> raise it until the "stateless app" work (PRD §5.1) lands.
## Architecture
```
browser / bdrive CLI
│ https
┌─────▼─────┐ metadata ┌──────────────┐
│ Cloud Run │◀────────────▶│ Cloud SQL │ (accounts, orgs,
│ bdrive │ unix socket │ Postgres │ projects, invites…)
│ (1 inst.) │ └──────────────┘
└─────┬─────┘
│ ADC (runtime SA)
┌─────▼─────┐
│ GCS │ blobs/ + journal/ (file content + sync log)
└───────────┘
```
## Prerequisites
- `gcloud` installed and logged in (`gcloud auth login`).
- A **billing account** id (`gcloud billing accounts list`) if the script
creates the project.
- Values: `PROJECT_ID`, `ADMIN_EMAIL`, `ADMIN_DOMAIN` (the rest have defaults).
## Run it
From the repo root:
```sh
PROJECT_ID=beardrive-prod \
BILLING_ACCOUNT=0X0X0X-0X0X0X-0X0X0X \
ADMIN_EMAIL=you@runbear.io \
ADMIN_DOMAIN=runbear.io \
REGION=us-central1 \
bash example/deploy/gcp-cloudrun.sh
```
The script: creates/links the project → enables APIs → creates the GCS bucket
and Cloud SQL instance → generates a DB password (stored in Secret Manager) →
writes the hub config to a secret → builds the image from the repo `Dockerfile`
via Cloud Build → deploys Cloud Run with the Cloud SQL socket, the config
secret mounted at `/config/config.json`, and a dedicated runtime service
account granted GCS + Cloud SQL access. It prints the service URL.
## First-run: bootstrap the admin, then lock down
The hub ships **invite-only by default**, but a brand-new hub has no accounts,
so the deploy config temporarily allows **domain-gated self-signup**
(`allowed_domains: [ADMIN_DOMAIN]`). Steps:
1. Open the printed URL → **Sign up** as `ADMIN_EMAIL` (must be on
`ADMIN_DOMAIN`). The account is active immediately and is a hub admin.
2. Create your org/projects and invite teammates from the UI.
3. **Tighten to invite-only:** edit the config secret to `"allow_signup": false`
and redeploy:
```sh
gcloud secrets versions access latest --secret bdrive-config > /tmp/c.json
# …set "allow_signup": false …
gcloud secrets versions add bdrive-config --data-file=/tmp/c.json
gcloud run services update bdrive --region "$REGION" # picks up latest secret
```
## Rough cost
- **Cloud Run**: scales to ~zero when idle (min-instances=1 keeps one warm;
set `--min-instances 0` to save more, at the cost of cold-start journal
folding on first hit). ~$515/mo warm.
- **Cloud SQL** `db-f1-micro`: ~$815/mo (smallest shared-core tier).
- **GCS**: pay per GB stored + egress. Cheap for text; consider a lifecycle
policy later.
## Notes / limits (single-instance build)
- `max-instances=1` is required. Metadata correctness depends on one writer.
- `BDRIVE_HOME=/tmp` is ephemeral on Cloud Run → the server's own device id
regenerates on cold start (cosmetic in history). Mount a volume later to
persist it.
- Large **downloads/sync** stream through Cloud Run (bounded by the request
timeout, up to 60 min). Uploads go direct to storage when the backend can
presign. See PRD §5.2 for offloading these at scale.
- Put a custom domain on the service via `gcloud run domain-mappings` (gives
managed TLS).
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# Deploy the BearDrive hub to Google Cloud Run (single instance) with Cloud SQL
# Postgres for metadata and a GCS bucket for blobs/journals.
#
# The hub is single-process for now, so max-instances=1 (in-memory caches
# assume one writer). Run from the repo root: bash example/deploy/gcp-cloudrun.sh
set -euo pipefail
# gcloud needs Python >= 3.10 (3.9 crashes `gcloud builds`); point it at a
# newer interpreter if your system default is old:
# export CLOUDSDK_PYTHON=$(command -v python3.11 || command -v python3.12)
# ---- fill these in -----------------------------------------------------------
PROJECT_ID="${PROJECT_ID:?set PROJECT_ID}" # dedicated GCP project id
BILLING_ACCOUNT="${BILLING_ACCOUNT:-}" # e.g. 0X0X0X-0X0X0X-0X0X0X (only needed if creating the project)
REGION="${REGION:-us-central1}"
ADMIN_EMAIL="${ADMIN_EMAIL:?set ADMIN_EMAIL}" # first hub admin
ADMIN_DOMAIN="${ADMIN_DOMAIN:?set ADMIN_DOMAIN}" # signup limited to this email domain for bootstrap, e.g. runbear.io
BRAND="${BRAND:-BearDrive}"
# ------------------------------------------------------------------------------
BUCKET="${BUCKET:-${PROJECT_ID}-bdrive}"
SQL_INSTANCE="${SQL_INSTANCE:-bdrive-pg}"
SQL_TIER="${SQL_TIER:-db-f1-micro}"
DB_NAME="bdrive"; DB_USER="bdrive"
SERVICE="${SERVICE:-bdrive}"
RUN_SA="bdrive-run@${PROJECT_ID}.iam.gserviceaccount.com"
CONN_NAME="${PROJECT_ID}:${REGION}:${SQL_INSTANCE}"
echo "== project =="
gcloud projects describe "$PROJECT_ID" >/dev/null 2>&1 || {
echo "creating project $PROJECT_ID"; gcloud projects create "$PROJECT_ID"
[ -n "$BILLING_ACCOUNT" ] && gcloud billing projects link "$PROJECT_ID" --billing-account "$BILLING_ACCOUNT"
}
gcloud config set project "$PROJECT_ID"
echo "== enable APIs =="
gcloud services enable run.googleapis.com sqladmin.googleapis.com storage.googleapis.com \
secretmanager.googleapis.com artifactregistry.googleapis.com cloudbuild.googleapis.com
echo "== GCS bucket for blobs/journals =="
gcloud storage buckets describe "gs://$BUCKET" >/dev/null 2>&1 || \
gcloud storage buckets create "gs://$BUCKET" --location "$REGION" --uniform-bucket-level-access
echo "== Cloud SQL Postgres (this takes several minutes) =="
gcloud sql instances describe "$SQL_INSTANCE" >/dev/null 2>&1 || \
gcloud sql instances create "$SQL_INSTANCE" --database-version POSTGRES_16 \
--edition ENTERPRISE --tier "$SQL_TIER" --region "$REGION" \
--storage-size 10 --storage-auto-increase
gcloud sql databases describe "$DB_NAME" --instance "$SQL_INSTANCE" >/dev/null 2>&1 || \
gcloud sql databases create "$DB_NAME" --instance "$SQL_INSTANCE"
DB_PASS="$(gcloud secrets versions access latest --secret bdrive-db-pass 2>/dev/null || true)"
if [ -z "$DB_PASS" ]; then
DB_PASS="$(openssl rand -base64 24 | tr -d '/+=')"
printf '%s' "$DB_PASS" | gcloud secrets create bdrive-db-pass --data-file=- 2>/dev/null || \
printf '%s' "$DB_PASS" | gcloud secrets versions add bdrive-db-pass --data-file=-
fi
gcloud sql users create "$DB_USER" --instance "$SQL_INSTANCE" --password "$DB_PASS" 2>/dev/null || \
gcloud sql users set-password "$DB_USER" --instance "$SQL_INSTANCE" --password "$DB_PASS"
echo "== runtime service account + IAM =="
gcloud iam service-accounts describe "$RUN_SA" >/dev/null 2>&1 || \
gcloud iam service-accounts create bdrive-run --display-name "BearDrive Cloud Run"
gcloud storage buckets add-iam-policy-binding "gs://$BUCKET" \
--member "serviceAccount:$RUN_SA" --role roles/storage.objectAdmin
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member "serviceAccount:$RUN_SA" --role roles/cloudsql.client >/dev/null
echo "== hub config secret (contains the DB DSN) =="
# Bootstrap posture: domain-gated self-signup so the first admin can create
# their account and become owner; tighten to invite-only afterwards.
CONFIG="$(cat <<JSON
{
"remote": "gs://$BUCKET/hub",
"addr": ":8080",
"upload": true,
"database": {
"driver": "postgres",
"dsn": "postgres://$DB_USER:$DB_PASS@/$DB_NAME?host=/cloudsql/$CONN_NAME&sslmode=disable"
},
"auth": {
"allow_signup": true,
"allowed_domains": ["$ADMIN_DOMAIN"],
"admins": ["$ADMIN_EMAIL"],
"brand": "$BRAND"
}
}
JSON
)"
printf '%s' "$CONFIG" | gcloud secrets create bdrive-config --data-file=- 2>/dev/null || \
printf '%s' "$CONFIG" | gcloud secrets versions add bdrive-config --data-file=-
gcloud secrets add-iam-policy-binding bdrive-config \
--member "serviceAccount:$RUN_SA" --role roles/secretmanager.secretAccessor >/dev/null
echo "== build + deploy to Cloud Run (single instance) =="
gcloud run deploy "$SERVICE" \
--source . \
--region "$REGION" \
--service-account "$RUN_SA" \
--add-cloudsql-instances "$CONN_NAME" \
--update-secrets "/config/config.json=bdrive-config:latest" \
--args "web,-c,/config/config.json" \
--min-instances 1 --max-instances 1 \
--cpu 1 --memory 512Mi \
--allow-unauthenticated
echo
echo "Deployed. URL:"
gcloud run services describe "$SERVICE" --region "$REGION" --format 'value(status.url)'
echo "Next: open the URL, Sign up as $ADMIN_EMAIL (domain-gated), then tighten"
echo "auth to invite-only by editing the bdrive-config secret + redeploying."