From a9cc43354d485ca949ceec69d2b636506dd9b9f7 Mon Sep 17 00:00:00 2001 From: Snow Lee Date: Fri, 10 Jul 2026 08:51:55 -0700 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs --- .dockerignore | 10 ++++ Dockerfile | 21 ++++++++ deploy/README.md | 90 +++++++++++++++++++++++++++++++++ deploy/gcp-cloudrun.sh | 110 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 231 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 deploy/README.md create mode 100755 deploy/gcp-cloudrun.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0d8dfe3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +bdrive +deploy +example +website +*.md +.goreleaser.yaml +.claude +plugin +/private diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c38e56e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# Build a static, CGO-free bdrive binary and ship it on distroless. +# The web server binds --addr (default :8080, which Cloud Run expects); GCS +# access uses Application Default Credentials (the runtime service account on +# Cloud Run / GCE — no key file needed). +FROM golang:1.26-alpine AS build +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +ARG VERSION=0.1.0-dev +RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=${VERSION}" -o /bdrive ./cmd/bdrive + +FROM gcr.io/distroless/static-debian12:nonroot +COPY --from=build /bdrive /bdrive +# Device identity + any file-backed state live here; mount a volume to persist +# it, or rely on a SQL database for metadata (recommended on Cloud Run). +ENV BDRIVE_HOME=/tmp/bdrive +ENTRYPOINT ["/bdrive"] +# Args are supplied at deploy time, e.g.: +# web s3://... --addr :8080 (flags) +# web -c /config/config.json (mounted config) diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..9b5e866 --- /dev/null +++ b/deploy/README.md @@ -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). ~$5–15/mo warm. +- **Cloud SQL** `db-f1-micro`: ~$8–15/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). diff --git a/deploy/gcp-cloudrun.sh b/deploy/gcp-cloudrun.sh new file mode 100755 index 0000000..6ec8e28 --- /dev/null +++ b/deploy/gcp-cloudrun.sh @@ -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 </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."