mirror of
https://github.com/saltbo/zpan.git
synced 2026-11-03 14:43:53 +08:00
184 lines
9.1 KiB
Markdown
184 lines
9.1 KiB
Markdown
# Google Cloud Run Deployment
|
||
|
||
ZPan supports Google Cloud Run as a first-class deploy target. It reuses the existing root `Dockerfile` — no separate entry file is needed. [Turso](https://turso.tech) provides the database, and any S3-compatible bucket (Cloudflare R2 recommended) handles file storage.
|
||
|
||
> **GCS is not adapted as a storage driver.** Google Cloud Storage is not S3-compatible at the API level. Bring an external S3-compatible bucket (R2, AWS S3, Tigris, B2, etc.).
|
||
|
||
## Prerequisites
|
||
|
||
| Tool / Account | Purpose |
|
||
|---------------|---------|
|
||
| [Google Cloud account](https://cloud.google.com) | Hosts the Cloud Run service |
|
||
| GCP project with billing enabled | Cloud Run, Cloud Build, Secret Manager, and Container Registry are used |
|
||
| [Turso database](https://turso.tech) | libSQL-compatible remote database |
|
||
| S3-compatible storage | File storage (Cloudflare R2 recommended — free egress) |
|
||
|
||
## Required Secrets
|
||
|
||
Set these in your fork's GitHub repository under **Settings → Secrets and variables → Actions**:
|
||
|
||
| Secret | Description |
|
||
|--------|-------------|
|
||
| `GCP_SERVICE_ACCOUNT_KEY` | JSON key for a GCP service account with roles: `roles/run.admin`, `roles/cloudbuild.builds.editor`, `roles/secretmanager.admin`, `roles/iam.serviceAccountUser`, `roles/storage.admin` |
|
||
| `GCP_PROJECT_ID` | Your GCP project ID (e.g. `my-zpan-project`) |
|
||
| `TURSO_DATABASE_URL` | Turso database URL, e.g. `libsql://your-db.turso.io` |
|
||
|
||
### Optional Secrets
|
||
|
||
| Secret | Description |
|
||
|--------|-------------|
|
||
| `TURSO_AUTH_TOKEN` | Turso auth token. Create with `turso db tokens create your-db`. Required for remote Turso URLs; omit for `file://` local databases. |
|
||
| `BETTER_AUTH_SECRET` | Signing secret for auth sessions. If not provided, the workflow auto-generates one on first deploy and stores it in Secret Manager. Back it up from the GCP console before rotating. To bring your own: `openssl rand -base64 32`. |
|
||
| `BETTER_AUTH_URL` | Custom domain for auth callbacks, e.g. `https://zpan.example.com`. If omitted, the workflow automatically uses the Cloud Run service URL assigned on first deploy. Only needed if you configure a custom domain after the initial deploy. |
|
||
|
||
## Quick Start (Fork + Deploy)
|
||
|
||
1. **Fork** the `saltbo/zpan` repository.
|
||
|
||
2. **Create a GCP project** and enable the required APIs:
|
||
```sh
|
||
gcloud projects create my-zpan-project --name="ZPan"
|
||
gcloud config set project my-zpan-project
|
||
gcloud services enable \
|
||
run.googleapis.com \
|
||
cloudbuild.googleapis.com \
|
||
secretmanager.googleapis.com \
|
||
containerregistry.googleapis.com
|
||
```
|
||
|
||
3. **Create a service account** and download its key:
|
||
```sh
|
||
gcloud iam service-accounts create zpan-deployer \
|
||
--display-name="ZPan Deployer"
|
||
|
||
for role in roles/run.admin roles/cloudbuild.builds.editor \
|
||
roles/secretmanager.admin roles/iam.serviceAccountUser \
|
||
roles/storage.admin; do
|
||
gcloud projects add-iam-policy-binding my-zpan-project \
|
||
--member="serviceAccount:zpan-deployer@my-zpan-project.iam.gserviceaccount.com" \
|
||
--role="$role"
|
||
done
|
||
|
||
gcloud iam service-accounts keys create gcp-key.json \
|
||
--iam-account="zpan-deployer@my-zpan-project.iam.gserviceaccount.com"
|
||
```
|
||
Use the contents of `gcp-key.json` as the `GCP_SERVICE_ACCOUNT_KEY` secret. Delete the local file afterward.
|
||
|
||
4. **Create a Turso database:**
|
||
```sh
|
||
turso db create zpan-db
|
||
turso db show zpan-db --url # → TURSO_DATABASE_URL
|
||
turso db tokens create zpan-db # → TURSO_AUTH_TOKEN (optional but recommended)
|
||
```
|
||
|
||
5. **Add the required secrets** to your fork (see table above). `BETTER_AUTH_URL` and `BETTER_AUTH_SECRET` are both optional — the workflow handles them automatically on first deploy.
|
||
|
||
6. **Push to `main`** — the `deploy-cloud-run.yml` workflow runs automatically. Cloud Build builds the image, Turso migrations run, `service.yaml` drives the Cloud Run deploy, and `BETTER_AUTH_URL` is auto-wired from the assigned service URL.
|
||
|
||
## Workflow Steps
|
||
|
||
The workflow follows the standard 8-step deployment contract:
|
||
|
||
1. **Guard** — skips on the upstream `saltbo/zpan` repo.
|
||
2. **Check required secrets** — fails early with an actionable error if any are missing.
|
||
3. **Resolve release tag** — defaults to the latest `saltbo/zpan` release; override via `workflow_dispatch` input.
|
||
4. **Checkout** upstream code at the resolved tag.
|
||
5. **Authenticate** to Google Cloud using the service account key.
|
||
6. **Apply Turso migrations** (`pnpm db:migrate`).
|
||
7. **Store required secrets** in Secret Manager (`turso-database-url`, `better-auth-secret`).
|
||
8. **Build + deploy** — Cloud Build builds the image; `gcloud run services replace deploy/cloud-run/service.yaml` creates or updates the service; a post-deploy step wires `BETTER_AUTH_URL` (from the real service URL) and `TURSO_AUTH_TOKEN` (if provided) into the running service via `gcloud run services update --update-secrets`.
|
||
|
||
### BETTER_AUTH_URL — first-deploy behaviour
|
||
|
||
`BETTER_AUTH_URL` is not known until after Cloud Run assigns the service URL. The workflow resolves this automatically:
|
||
|
||
1. The initial `gcloud run services replace` deploys without `BETTER_AUTH_URL`.
|
||
2. After deploy, the service URL is captured and stored as the `better-auth-url` Secret Manager entry.
|
||
3. `gcloud run services update --update-secrets` wires it into the running service in-place.
|
||
|
||
No manual intervention is required on first deploy. If you later configure a custom domain, set `BETTER_AUTH_URL` as a GitHub secret and re-run the workflow — it will update the secret and redeploy.
|
||
|
||
## Region
|
||
|
||
The default region is `us-central1`. To deploy to a different region, use the `workflow_dispatch` trigger and set the `region` input:
|
||
|
||
```
|
||
Actions → Deploy to Google Cloud Run → Run workflow → region: europe-west1
|
||
```
|
||
|
||
## Cold-Start Expectations
|
||
|
||
`min-instances` is set to **0** to qualify for the free tier. With zero minimum instances, the first request after a period of inactivity will experience a cold start — typically **1–3 seconds** for the ZPan Node.js container.
|
||
|
||
To eliminate cold starts at the cost of always-on billing, set `min-instances` to `1` in `deploy/cloud-run/service.yaml` before deploying.
|
||
|
||
## Service Manifest
|
||
|
||
`deploy/cloud-run/service.yaml` is the Knative service manifest that drives automated deployments. The workflow substitutes the `PROJECT_ID` placeholder and pins the image tag before calling `gcloud run services replace`.
|
||
|
||
To validate the manifest manually before deploying:
|
||
|
||
```sh
|
||
# Substitute PROJECT_ID and validate (dry-run)
|
||
sed "s/PROJECT_ID/my-zpan-project/g" deploy/cloud-run/service.yaml | \
|
||
gcloud run services replace - --region us-central1 --dry-run
|
||
```
|
||
|
||
`TURSO_AUTH_TOKEN` and `BETTER_AUTH_URL` are not in the base manifest because they are optional or not known at initial deploy time. The workflow adds them post-deploy via `gcloud run services update --update-secrets`.
|
||
|
||
## Storage Configuration
|
||
|
||
Cloud Run deployments require an **external S3-compatible bucket**. Configure it via the ZPan admin dashboard after your first login. Cloudflare R2 is recommended — it has zero egress fees, and there are no bandwidth charges between Cloud Run and R2.
|
||
|
||
GCS (Google Cloud Storage) is **not supported** as a storage backend — it uses a different API protocol.
|
||
|
||
## Pricing Notes
|
||
|
||
- **Cloud Run** — free tier: 2 million requests/month, 360,000 GB-seconds compute, 180,000 vCPU-seconds. With `min-instances=0`, idle deployments cost $0.
|
||
- **Cloud Build** — free tier: 120 build-minutes/day. Each deployment triggers one Cloud Build run.
|
||
- **Secret Manager** — free tier: 6 active secret versions, 10,000 access operations/month.
|
||
- **Turso** — free tier: 500 databases, 9 GB total storage, 1B row reads / 25M row writes per month.
|
||
- **Cloudflare R2** — 10 GB storage free, zero egress fees.
|
||
|
||
A personal ZPan instance with light usage fits entirely within free tiers across all services.
|
||
|
||
---
|
||
|
||
## Entitlement Refresh (License Cert)
|
||
|
||
ZPan refreshes its entitlement certificate every 6 hours. On Cloud Run there is no persistent background process between requests, so you need to trigger a refresh via an external scheduler.
|
||
|
||
### Setup
|
||
|
||
1. **Generate a secret:**
|
||
```sh
|
||
openssl rand -hex 32
|
||
```
|
||
|
||
2. **Add the env var** as a Cloud Run environment variable (or via Secret Manager). With `gcloud`:
|
||
```sh
|
||
gcloud run services update zpan \
|
||
--region <your-region> \
|
||
--update-env-vars REFRESH_CRON_SECRET=<your-secret>
|
||
```
|
||
|
||
3. **Schedule the calls** using [Cloud Scheduler](https://cloud.google.com/scheduler). Create the license refresh job:
|
||
```sh
|
||
gcloud scheduler jobs create http zpan-license-refresh \
|
||
--schedule="0 */6 * * *" \
|
||
--uri="https://<your-cloud-run-url>/api/licensing/refresh-cron?secret=<REFRESH_CRON_SECRET>" \
|
||
--http-method=POST \
|
||
--location=<your-region>
|
||
```
|
||
|
||
Create the traffic sync job:
|
||
```sh
|
||
gcloud scheduler jobs create http zpan-traffic-sync \
|
||
--schedule="*/10 * * * *" \
|
||
--uri="https://<your-cloud-run-url>/api/licensing/traffic-sync-runs?secret=<REFRESH_CRON_SECRET>" \
|
||
--http-method=POST \
|
||
--location=<your-region>
|
||
```
|
||
|
||
If `REFRESH_CRON_SECRET` is not set, the endpoint returns `401` for all requests.
|