Skip to main content

Overview

Kuest Prediction Market can be deployed to various cloud providers beyond Vercel. This guide covers deployment to Google Cloud Run, Fly.io, DigitalOcean App Platform, and generic VPS providers.
All cloud deployments use the same Docker image from infra/docker/Dockerfile and support both Supabase and Postgres+S3 storage modes.

Google Cloud Run

Google Cloud Run provides fully managed, serverless containers with automatic scaling.

Prerequisites

  1. GCP project with billing enabled
  2. gcloud CLI installed and authenticated
  3. Required APIs enabled:
    • Cloud Build API
    • Artifact Registry API
    • Cloud Run Admin API
    • Secret Manager API

Setup

1
Create Secrets in Secret Manager
2
Create all required secrets:
3
echo -n "your-postgres-url" | gcloud secrets create POSTGRES_URL --data-file=-
echo -n "your-cron-secret" | gcloud secrets create CRON_SECRET --data-file=-
echo -n "your-auth-secret" | gcloud secrets create BETTER_AUTH_SECRET --data-file=-
echo -n "your-reown-id" | gcloud secrets create REOWN_APPKIT_PROJECT_ID --data-file=-
echo -n "0x123..." | gcloud secrets create ADMIN_WALLETS --data-file=-
echo -n "0x456..." | gcloud secrets create KUEST_ADDRESS --data-file=-
echo -n "your-api-key" | gcloud secrets create KUEST_API_KEY --data-file=-
echo -n "your-api-secret" | gcloud secrets create KUEST_API_SECRET --data-file=-
echo -n "your-passphrase" | gcloud secrets create KUEST_PASSPHRASE --data-file=-
4
For Supabase mode, add:
5
echo -n "https://xxx.supabase.co" | gcloud secrets create SUPABASE_URL --data-file=-
echo -n "eyJhbGc..." | gcloud secrets create SUPABASE_SERVICE_ROLE_KEY --data-file=-
6
For S3 mode, add:
7
echo -n "kuest-assets" | gcloud secrets create S3_BUCKET --data-file=-
echo -n "your-key-id" | gcloud secrets create S3_ACCESS_KEY_ID --data-file=-
echo -n "your-secret-key" | gcloud secrets create S3_SECRET_ACCESS_KEY --data-file=-
8
Deploy with Cloud Build
9
The repository includes a complete Cloud Build configuration:
10
Supabase Mode
gcloud builds submit --config infra/cloud-run/cloudbuild.yaml \
  --substitutions=_SERVICE=kuest-web,_REGION=us-central1,_SITE_URL=https://markets.example.com,_STORAGE_MODE=supabase
S3 Mode
gcloud builds submit --config infra/cloud-run/cloudbuild.yaml \
  --substitutions=_SERVICE=kuest-web,_REGION=us-central1,_SITE_URL=https://markets.example.com,_STORAGE_MODE=s3,_S3_ENDPOINT=https://s3.amazonaws.com,_S3_REGION=us-east-1
11
This will:
12
  • Build the Docker image from infra/docker/Dockerfile
  • Push to Artifact Registry
  • Deploy to Cloud Run with secrets configured
  • 13
    Configure Custom Domain
    14
    gcloud run services add-iam-policy-binding kuest-web \
      --region=us-central1 \
      --member="allUsers" \
      --role="roles/run.invoker"
    
    gcloud run services update kuest-web \
      --region=us-central1 \
      --custom-audience="https://markets.example.com"
    
    15
    Then add domain mapping in Cloud Run console.
    16
    Setup Cloud Scheduler (S3 mode only)
    17
    Only create these scheduler jobs if using Postgres + S3 mode. Skip if using Supabase mode.
    18
    SITE_URL=https://markets.example.com
    CRON_SECRET=your-cron-secret
    
    gcloud scheduler jobs create http kuest-sync-events \
      --location=us-central1 \
      --schedule="1-59/5 * * * *" \
      --uri="${SITE_URL}/api/sync/events" \
      --http-method=GET \
      --headers="Authorization=Bearer ${CRON_SECRET}"
    
    gcloud scheduler jobs create http kuest-sync-resolution \
      --location=us-central1 \
      --schedule="3-59/5 * * * *" \
      --uri="${SITE_URL}/api/sync/resolution" \
      --http-method=GET \
      --headers="Authorization=Bearer ${CRON_SECRET}"
    
    gcloud scheduler jobs create http kuest-sync-translations \
      --location=us-central1 \
      --schedule="*/10 * * * *" \
      --uri="${SITE_URL}/api/sync/translations" \
      --http-method=GET \
      --headers="Authorization=Bearer ${CRON_SECRET}"
    
    gcloud scheduler jobs create http kuest-sync-volume \
      --location=us-central1 \
      --schedule="14,44 * * * *" \
      --uri="${SITE_URL}/api/sync/volume" \
      --http-method=GET \
      --headers="Authorization=Bearer ${CRON_SECRET}"
    

    Cloud Build Configuration

    The infra/cloud-run/cloudbuild.yaml includes:
    steps:
      # Validation step
      - name: gcr.io/google.com/cloudsdktool/cloud-sdk:slim
        entrypoint: bash
        args:
          - -ceu
          - |
            # Validates required substitutions and storage mode
            
      # Build Docker image
      - name: gcr.io/cloud-builders/docker
        args:
          - build
          - -f
          - infra/docker/Dockerfile
          - -t
          - ${_AR_REGION}-docker.pkg.dev/$PROJECT_ID/${_AR_REPOSITORY}/${_IMAGE_NAME}:${SHORT_SHA}
          - .
    
      # Push to Artifact Registry
      - name: gcr.io/cloud-builders/docker
        args:
          - push
          - ${_AR_REGION}-docker.pkg.dev/$PROJECT_ID/${_AR_REPOSITORY}/${_IMAGE_NAME}:${SHORT_SHA}
    
      # Deploy to Cloud Run
      - name: gcr.io/google.com/cloudsdktool/cloud-sdk:slim
        entrypoint: bash
        args:
          - -ceu
          - |
            # Dynamically configures secrets and env vars based on storage mode
            gcloud run deploy "${_SERVICE}" \
              --project "$PROJECT_ID" \
              --region "${_REGION}" \
              --platform managed \
              --image "${_AR_REGION}-docker.pkg.dev/$PROJECT_ID/${_AR_REPOSITORY}/${_IMAGE_NAME}:${SHORT_SHA}" \
              --allow-unauthenticated \
              --port 3000 \
              --set-env-vars "${runtime_env_csv}" \
              --update-secrets "${secret_env_csv}"
    
    substitutions:
      _SERVICE: kuest-web
      _REGION: us-central1
      _SITE_URL: https://markets.example.com
      _STORAGE_MODE: supabase
    

    Fly.io

    Fly.io provides edge deployment with global distribution.

    Prerequisites

    1. Fly.io account
    2. flyctl CLI installed and authenticated
    3. Fly app created: fly apps create <app-name>

    Setup

    1
    Configure Secrets
    2
    fly secrets set \
      KUEST_ADDRESS="0x..." \
      KUEST_API_KEY="xxx" \
      KUEST_API_SECRET="xxx" \
      KUEST_PASSPHRASE="xxx" \
      ADMIN_WALLETS="0x..." \
      REOWN_APPKIT_PROJECT_ID="xxx" \
      BETTER_AUTH_SECRET="xxx" \
      CRON_SECRET="xxx" \
      POSTGRES_URL="postgresql://..." \
      SUPABASE_URL="https://xxx.supabase.co" \
      SUPABASE_SERVICE_ROLE_KEY="eyJhbGc..." \
      SITE_URL="https://your-app.fly.dev" \
      --app <app-name>
    
    3
    Deploy Image
    4
    flyctl deploy \
      --app <app-name> \
      --config infra/fly/fly.toml \
      --image ghcr.io/kuestcom/prediction-market@sha256:<digest>
    
    5
    Configure Custom Domain
    6
    fly certs create markets.example.com --app <app-name>
    fly ips list --app <app-name>
    
    7
    Add DNS records:
    8
  • A record: @ → Fly IPv4
  • AAAA record: @ → Fly IPv6
  • Fly.io Configuration

    app = "kuest-web"
    primary_region = "iad"
    
    [http_service]
    internal_port = 3000
    force_https = true
    auto_start_machines = true
    auto_stop_machines = "off"
    min_machines_running = 1
    processes = [ "app" ]
    
    [[vm]]
    cpu_kind = "shared"
    cpus = 1
    memory_mb = 1024
    
    [env]
    NODE_ENV = "production"
    
    Fly.io deployment requires an external scheduler for /api/sync/* endpoints when not using Supabase mode. Use GitHub Actions, Cloud Scheduler, or any cron service.

    DigitalOcean

    DigitalOcean offers both App Platform (PaaS) and Droplets (VPS).

    App Platform

    1
    Create App from GitHub
    2
  • Open DigitalOcean Dashboard
  • Navigate to AppsCreate App
  • Choose GitHub as source
  • Select repository: <your-username>/prediction-market
  • Select branch: main
  • 3
    Configure Build Settings
    4
  • Type: Web Service
  • Source type: Dockerfile
  • Dockerfile path: infra/docker/Dockerfile
  • Build context: .
  • HTTP port: 3000
  • 5
    Set Environment Variables
    6
    In App Platform settings, add all required variables:
    7
    KUEST_ADDRESS=0x...
    KUEST_API_KEY=xxx
    KUEST_API_SECRET=xxx
    KUEST_PASSPHRASE=xxx
    ADMIN_WALLETS=0x...
    REOWN_APPKIT_PROJECT_ID=xxx
    BETTER_AUTH_SECRET=xxx
    CRON_SECRET=xxx
    POSTGRES_URL=postgresql://...
    SUPABASE_URL=https://xxx.supabase.co
    SUPABASE_SERVICE_ROLE_KEY=eyJhbGc...
    SITE_URL=https://your-app.ondigitalocean.app
    NODE_ENV=production
    
    8
    Mark sensitive values as encrypted.
    9
    Deploy
    10
  • Click Create Resources
  • Wait for build and deployment
  • Verify at generated URL
  • 11
    Add Custom Domain
    12
  • Go to app SettingsDomains
  • Add your domain
  • Follow DNS configuration instructions
  • Update SITE_URL environment variable
  • VPS Deployment (Droplets, Vultr, Hetzner, EC2)

    For VPS providers, use Docker Compose with production configuration.
    1
    Provision VPS
    2
  • OS: Ubuntu 22.04+ recommended
  • Minimum: 2 CPU, 4GB RAM, 40GB storage
  • Open ports: 22 (SSH), 80 (HTTP), 443 (HTTPS)
  • 3
    Initial Server Setup
    4
    # Update system
    sudo apt update && sudo apt upgrade -y
    sudo timedatectl set-timezone UTC
    
    # Configure firewall
    sudo ufw allow OpenSSH
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw --force enable
    
    5
    Install Docker
    6
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    
    sudo apt-get update
    sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin git
    
    sudo usermod -aG docker "$USER"
    newgrp docker
    
    7
    Clone and Configure
    8
    sudo mkdir -p /opt/kuest
    sudo chown "$USER":"$USER" /opt/kuest
    cd /opt/kuest
    git clone https://github.com/<your-org>/prediction-market.git
    cd prediction-market
    cp .env.example .env
    
    9
    Edit .env with your configuration, including:
    10
    CADDY_DOMAIN=markets.example.com
    SITE_URL=https://markets.example.com
    
    11
    Deploy with Docker Compose
    12
    Supabase Mode
    cd /opt/kuest/prediction-market
    docker compose --env-file .env -f infra/docker/docker-compose.production.yml up -d --build
    
    Postgres+S3 with Local PostgreSQL
    cd /opt/kuest/prediction-market
    docker compose --env-file .env -f infra/docker/docker-compose.production.yml --profile local-postgres up -d --build
    
    13
    Run Migrations
    14
    docker compose --env-file .env -f infra/docker/docker-compose.production.yml exec web npm run db:push
    
    15
    Setup Cron Jobs (S3 mode only)
    16
    Only configure cron if using Postgres + S3 mode. Skip for Supabase mode.
    17
    crontab -e
    
    18
    Add:
    19
    1-59/5 * * * * curl -fsS -H "Authorization: Bearer your-cron-secret" "https://markets.example.com/api/sync/events" >/dev/null 2>&1
    3-59/5 * * * * curl -fsS -H "Authorization: Bearer your-cron-secret" "https://markets.example.com/api/sync/resolution" >/dev/null 2>&1
    */10 * * * * curl -fsS -H "Authorization: Bearer your-cron-secret" "https://markets.example.com/api/sync/translations" >/dev/null 2>&1
    14,44 * * * * curl -fsS -H "Authorization: Bearer your-cron-secret" "https://markets.example.com/api/sync/volume" >/dev/null 2>&1
    

    VPS Management

    cd /opt/kuest/prediction-market
    docker compose -f infra/docker/docker-compose.production.yml ps
    

    Storage Mode Comparison

    FeatureSupabase ModePostgres + S3 Mode
    DatabaseSupabase PostgreSQLAny PostgreSQL
    Object StorageSupabase StorageS3-compatible
    Cron SchedulerBuilt-in (Supabase)External required
    ComplexityLowerHigher
    Vendor Lock-inModerateLower
    CostFree tier availablePay per resource

    Supabase Mode

    Required variables:
    POSTGRES_URL=postgresql://...
    SUPABASE_URL=https://xxx.supabase.co
    SUPABASE_SERVICE_ROLE_KEY=eyJhbGc...
    
    Benefits:
    • Automatic cron scheduling
    • Integrated storage and database
    • Generous free tier

    Postgres + S3 Mode

    Required variables:
    POSTGRES_URL=postgresql://...
    S3_BUCKET=kuest-assets
    S3_ACCESS_KEY_ID=xxx
    S3_SECRET_ACCESS_KEY=xxx
    
    Optional S3 settings:
    S3_ENDPOINT=https://s3.amazonaws.com
    S3_REGION=us-east-1
    S3_PUBLIC_URL=https://cdn.example.com
    S3_FORCE_PATH_STYLE=false
    
    Benefits:
    • Full control over infrastructure
    • Any S3-compatible provider (AWS, MinIO, Backblaze, etc.)
    • Better for high-scale deployments
    Requirements:
    • Must implement scheduler contract for /api/sync/* endpoints
    • See scheduler options: Cloud Scheduler, GitHub Actions, cron jobs

    Troubleshooting

    Image Pull Failures

    Cloud Run/GCP:
    # Verify Artifact Registry permissions
    gcloud projects get-iam-policy $PROJECT_ID
    
    # Enable required APIs
    gcloud services enable artifactregistry.googleapis.com
    
    Fly.io:
    # Use public image or configure registry
    fly secrets set FLY_REGISTRY_TOKEN=$(fly auth token)
    

    Environment Variables Not Loading

    1. Verify secrets are created in provider’s secret manager
    2. Check case sensitivity (some providers are case-sensitive)
    3. Redeploy after adding new variables
    4. Check logs for missing variable errors

    Domain SSL Issues

    1. Verify DNS propagation: dig markets.example.com
    2. Wait 24-48 hours for full propagation
    3. Check SSL certificate status in provider dashboard
    4. Ensure domain verification is complete

    Scheduler Not Running

    1. Verify CRON_SECRET matches in scheduler and app
    2. Check scheduler logs in cloud provider console
    3. Test endpoint manually with curl
    4. Ensure scheduler has network access to app

    Cost Optimization

    Cloud Run

    • Use minimum instances (0 or 1)
    • Set max instances to prevent runaway costs
    • Enable CPU throttling
    • Use shared CPU tier

    Fly.io

    • Use auto_stop_machines = "suspend" for dev environments
    • Single region for lower costs
    • Shared CPU instances

    DigitalOcean

    • Start with Basic tier ($5/month droplet)
    • Use managed database free tier
    • Enable CDN for static assets

    VPS General

    • Use reserved instances for 30-50% savings
    • Monitor resource usage and downsize if possible
    • Use object storage for media (cheaper than disk)
    • Enable log rotation to save disk space

    Next Steps

    Build docs developers (and LLMs) love