Self-Hosting
This guide walks through deploying Rivano to your own GCP project using Pulumi for infrastructure management. You need a GCP account, billing enabled, and the gcloud and pulumi CLIs installed.
Prerequisites
- GCP project with billing enabled
gcloudCLI authenticated (gcloud auth login)pulumiCLI installed (npm i -g pulumi)- Node.js 20+, Docker
- A domain you control for DNS
Step 1: Enable GCP APIs
gcloud services enable \
run.googleapis.com \
sqladmin.googleapis.com \
artifactregistry.googleapis.com \
secretmanager.googleapis.com \
cloudresourcemanager.googleapis.com \
iam.googleapis.com \
--project=YOUR_PROJECT_ID
Step 2: Create Artifact Registry
gcloud artifacts repositories create rivano \
--repository-format=docker \
--location=us-east1 \
--project=YOUR_PROJECT_ID
Step 3: Set up Cloud SQL
gcloud sql instances create rivano-db \
--database-version=POSTGRES_16 \
--tier=db-f1-micro \
--region=us-east1 \
--storage-auto-increase \
--backup \
--project=YOUR_PROJECT_ID
gcloud sql databases create rivano \
--instance=rivano-db \
--project=YOUR_PROJECT_ID
# Create the application user
gcloud sql users create rivano \
--instance=rivano-db \
--password=YOUR_DB_PASSWORD \
--project=YOUR_PROJECT_ID
Note the Cloud SQL instance connection name: YOUR_PROJECT_ID:us-east1:rivano-db.
Step 4: Store secrets in Secret Manager
# JWT secret (min 32 characters)
echo -n "your-jwt-secret-min-32-chars" | \
gcloud secrets create jwt-secret --data-file=- --project=YOUR_PROJECT_ID
# Provider encryption key (exactly 32 bytes, HKDF-derived)
echo -n "your-32-byte-provider-key" | \
gcloud secrets create provider-encryption-key --data-file=- --project=YOUR_PROJECT_ID
# Stripe keys
echo -n "sk_live_..." | \
gcloud secrets create stripe-secret-key --data-file=- --project=YOUR_PROJECT_ID
echo -n "whsec_..." | \
gcloud secrets create stripe-webhook-secret --data-file=- --project=YOUR_PROJECT_ID
Step 5: Build and push Docker images
# Authenticate Docker to Artifact Registry
gcloud auth configure-docker us-east1-docker.pkg.dev
# Build and push control-plane
docker build --platform linux/amd64 \
-t us-east1-docker.pkg.dev/YOUR_PROJECT_ID/rivano/control-plane:latest \
-f apps/control-plane/Dockerfile .
docker push us-east1-docker.pkg.dev/YOUR_PROJECT_ID/rivano/control-plane:latest
# Build and push data-plane
docker build --platform linux/amd64 \
-t us-east1-docker.pkg.dev/YOUR_PROJECT_ID/rivano/data-plane:latest \
-f apps/data-plane/Dockerfile .
docker push us-east1-docker.pkg.dev/YOUR_PROJECT_ID/rivano/data-plane:latest
Step 6: Deploy with Pulumi
cd infra/
pulumi stack init production
pulumi config set gcp:project YOUR_PROJECT_ID
pulumi config set gcp:region us-east1
pulumi config set rivano:dbConnectionName YOUR_PROJECT_ID:us-east1:rivano-db
pulumi config set --secret rivano:dbPassword YOUR_DB_PASSWORD
pulumi up
The Pulumi program creates:
- Cloud Run services for control-plane and data-plane
- Cloud Run service for Zitadel (OIDC)
- Service accounts with least-privilege IAM bindings
- Secret Manager access grants
- Cloud SQL Auth Proxy sidecar bindings
⚠
Always use --platform linux/amd64 when building Docker images on Apple Silicon. Cloud Run runs on AMD64.
Step 7: Run database migrations
# Get the Cloud Run URL for control-plane
CONTROL_PLANE_URL=$(gcloud run services describe rivano-control-plane \
--region=us-east1 --format='value(status.url)' --project=YOUR_PROJECT_ID)
# Trigger migration endpoint (requires admin key)
curl -X POST $CONTROL_PLANE_URL/admin/migrate \
-H "Authorization: Bearer $ADMIN_SECRET"
Or connect directly to Cloud SQL via Cloud SQL Proxy and run migrations manually:
cloud-sql-proxy YOUR_PROJECT_ID:us-east1:rivano-db &
DATABASE_URL="postgresql://rivano:[email protected]:5432/rivano" \
pnpm --filter=control-plane db:migrate
Step 8: Configure DNS
Point your domains at the Cloud Run services using custom domain mappings:
gcloud run domain-mappings create \
--service=rivano-control-plane \
--domain=api.yourdomain.com \
--region=us-east1 \
--project=YOUR_PROJECT_ID
gcloud run domain-mappings create \
--service=rivano-data-plane \
--domain=gateway.yourdomain.com \
--region=us-east1 \
--project=YOUR_PROJECT_ID
Add the CNAME records Cloud Run provides to your DNS provider.
Step 9: Deploy the dashboard
The dashboard is a Next.js app deployable to Cloudflare Pages or any static host:
# Cloudflare Pages
cd apps/dashboard
NEXT_PUBLIC_API_URL=https://api.yourdomain.com \
pnpm build
# Deploy via Wrangler
command npx wrangler pages deploy .next/standalone \
--project-name=rivano-dashboard \
--branch=production
CI/CD with GitHub Actions
A sample workflow for automated deploys:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Build and push
run: |
gcloud auth configure-docker us-east1-docker.pkg.dev
docker build --platform linux/amd64 \
-t us-east1-docker.pkg.dev/${{ vars.GCP_PROJECT }}/rivano/control-plane:${{ github.sha }} \
-f apps/control-plane/Dockerfile .
docker push us-east1-docker.pkg.dev/${{ vars.GCP_PROJECT }}/rivano/control-plane:${{ github.sha }}
- name: Deploy to Cloud Run
run: |
gcloud run deploy rivano-control-plane \
--image=us-east1-docker.pkg.dev/${{ vars.GCP_PROJECT }}/rivano/control-plane:${{ github.sha }} \
--region=us-east1 \
--project=${{ vars.GCP_PROJECT }}
Related
- Hybrid Deployment — Self-hosted gateway only (not full stack)