Files
zpan/docs/deploy/cloud-run.md
T

9.1 KiB
Raw Blame History

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 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 Hosts the Cloud Run service
GCP project with billing enabled Cloud Run, Cloud Build, Secret Manager, and Container Registry are used
Turso database 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:

    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:

    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:

    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 13 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:

# 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:

    openssl rand -hex 32
    
  2. Add the env var as a Cloud Run environment variable (or via Secret Manager). With gcloud:

    gcloud run services update zpan \
      --region <your-region> \
      --update-env-vars REFRESH_CRON_SECRET=<your-secret>
    
  3. Schedule the calls using Cloud Scheduler. Create the license refresh job:

    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:

    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.