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
  • gcloud CLI authenticated (gcloud auth login)
  • pulumi CLI 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 }}