mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
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:
co-authored by
Claude Opus 4.8
parent
62175e29a9
commit
a9cc43354d
@@ -0,0 +1,10 @@
|
||||
.git
|
||||
bdrive
|
||||
deploy
|
||||
example
|
||||
website
|
||||
*.md
|
||||
.goreleaser.yaml
|
||||
.claude
|
||||
plugin
|
||||
/private
|
||||
+21
@@ -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)
|
||||
@@ -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).
|
||||
Executable
+110
@@ -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."
|
||||
Reference in New Issue
Block a user