This runbook provides step-by-step, copy-paste runnable instructions for deploying LessonArcade to Google Cloud Run.
For the fastest path to deploy and get your hosted URL for Devpost, use the provided deployment scripts:
# 1. Set your project ID (required)
export GCP_PROJECT_ID="your-project-id"
# 2. Enable required APIs (one-time setup)
gcloud services enable run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com secretmanager.googleapis.com aiplatform.googleapis.com
# 3. Grant Vertex AI permissions to Cloud Run service account (one-time setup)
PROJECT_NUMBER=$(gcloud projects describe $GCP_PROJECT_ID --format='value(projectNumber)')
SERVICE_ACCOUNT="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT" \
--role="roles/aiplatform.user"
# 4. Run the deployment script
./scripts/cloud-run/deploy.sh
# The script will output: HOSTED_URL=https://your-service-url.a.run.app
# Use this URL for your Devpost submission
# 5. Quick URL retrieval (after deployment)
# The fastest way to get the URL for Devpost without committing it:
pnpm hosted:urlOptional: Run smoke tests after deployment:
# Get the URL from the deploy script output or:
SERVICE_URL=$(gcloud run services describe lessonarcade --region=us-central1 --format="value(status.url)")
# Run smoke tests
./scripts/cloud-run/smoke-test.sh $SERVICE_URLBefore you begin, ensure you have:
-
Google Cloud SDK (gcloud) installed
# Install gcloud CLI (macOS) brew install google-cloud-sdk # Initialize gcloud gcloud init
-
Docker installed and running
# Verify Docker is running docker ps -
A Google Cloud project with billing enabled
# Create or select a project gcloud projects create PROJECT_ID gcloud config set project PROJECT_ID
-
Appropriate permissions (roles):
roles/run.admin- Cloud Run Adminroles/artifactregistry.writer- Artifact Registry Writerroles/secretmanager.admin- Secret Manager Adminroles/serviceusage.serviceUsageAdmin- Enable APIs
Enable the Cloud Run, Cloud Build, Artifact Registry, Secret Manager, and Vertex AI APIs:
# Set your project ID
export PROJECT_ID="your-project-id"
gcloud config set project $PROJECT_ID
# Enable required APIs
gcloud services enable run.googleapis.com
gcloud services enable cloudbuild.googleapis.com
gcloud services enable artifactregistry.googleapis.com
gcloud services enable secretmanager.googleapis.com
gcloud services enable aiplatform.googleapis.comCreate a Docker repository in Artifact Registry:
# Set your region
export REGION="us-central1"
export AR_REPO="lessonarcade"
# Create the repository
gcloud artifacts repositories create $AR_REPO \
--repository-format=docker \
--location=$REGION \
--description="Docker repository for LessonArcade"# Configure Docker authentication
gcloud auth configure-docker $REGION-docker.pkg.dev# Authenticate with gcloud
gcloud auth login
# For automated deployments, set up application default login
gcloud auth application-default loginFor Vertex AI integration, grant the Cloud Run service account the necessary permissions:
# Get the project number and service account
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
SERVICE_ACCOUNT="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
# Grant Vertex AI User role to the Cloud Run service account
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT" \
--role="roles/aiplatform.user"
# Verify the permission was granted
gcloud projects get-iam-policy $PROJECT_ID \
--filter="serviceAccount:$SERVICE_ACCOUNT" \
--flatten="bindings[].members" \
--format="table(bindings.role,bindings.members)"Note: The roles/aiplatform.user role allows the service account to invoke Vertex AI models. This is the minimum required permission for Vertex AI operations.
IMPORTANT: Never commit secrets to git. Use Google Cloud Secret Manager for sensitive values.
Create secrets for sensitive values:
# Create secrets
gcloud secrets create gemini-api-key --replication-policy="automatic"
gcloud secrets create elevenlabs-api-key --replication-policy="automatic"
gcloud secrets create studio-auth-user --replication-policy="automatic"
gcloud secrets create studio-auth-pass --replication-policy="automatic"
gcloud secrets create logging-salt --replication-policy="automatic"
# Add secret values (replace with your actual values)
echo -n "your-gemini-api-key" | gcloud secrets versions add gemini-api-key --data-file=-
echo -n "your-elevenlabs-api-key" | gcloud secrets versions add elevenlabs-api-key --data-file=-
echo -n "admin" | gcloud secrets versions add studio-auth-user --data-file=-
echo -n "your-secure-password" | gcloud secrets versions add studio-auth-pass --data-file=-
echo -n "random-salt-string" | gcloud secrets versions add logging-salt --data-file=-
# Grant Cloud Run service account access to secrets
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
SERVICE_ACCOUNT="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
gcloud secrets add-iam-policy-binding gemini-api-key \
--member="serviceAccount:$SERVICE_ACCOUNT" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding elevenlabs-api-key \
--member="serviceAccount:$SERVICE_ACCOUNT" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding studio-auth-user \
--member="serviceAccount:$SERVICE_ACCOUNT" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding studio-auth-pass \
--member="serviceAccount:$SERVICE_ACCOUNT" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding logging-salt \
--member="serviceAccount:$SERVICE_ACCOUNT" \
--role="roles/secretmanager.secretAccessor"For non-sensitive configuration values, you can use environment variables directly:
# Voice preset IDs (these are not secrets)
export VOICE_TTS_VOICE_ID_EN_INSTRUCTOR="your-voice-id"
export VOICE_TTS_VOICE_ID_EN_NARRATOR="your-voice-id"
export VOICE_TTS_VOICE_ID_ZH_INSTRUCTOR="your-voice-id"
export VOICE_TTS_VOICE_ID_ZH_NARRATOR="your-voice-id"LessonArcade supports two modes for Gemini AI integration:
For local development and testing, use the Google AI Studio API key:
# Set GEMINI_API_KEY as a secret
echo -n "your-gemini-api-key" | gcloud secrets versions add gemini-api-key --data-file=-For production deployments, use Vertex AI with Application Default Credentials (ADC):
# Set Vertex AI configuration as environment variables (not secrets)
export GCP_PROJECT_ID=$PROJECT_ID
export GCP_REGION="us-central1"
export GCP_VERTEX_MODEL="gemini-2.0-flash-exp"Benefits of Vertex AI Mode:
- Uses Application Default Credentials (ADC) for authentication
- No API key management required
- Better security and scalability
- Seamless integration with Cloud Run service accounts
- Enterprise-grade access controls
The project includes a deployment script that automates the entire process:
# Make the script executable (if not already)
chmod +x scripts/cloud-run/deploy.sh
# Set environment variables for deployment
export GCP_PROJECT_ID="your-project-id"
export GCP_REGION="us-central1" # Optional, defaults to us-central1
export CLOUD_RUN_SERVICE="lessonarcade" # Optional, defaults to lessonarcade
# Set required environment variables (non-sensitive ones)
export STUDIO_BASIC_AUTH_USER="admin"
export STUDIO_BASIC_AUTH_PASS="secure-password"
export LOGGING_SALT="random-salt-string"
# Set Vertex AI configuration (production mode)
export GCP_PROJECT_ID=$GCP_PROJECT_ID
export GCP_REGION="us-central1"
export GCP_VERTEX_MODEL="gemini-2.0-flash-exp"
# Set API keys (optional - can also use Secret Manager)
export ELEVENLABS_API_KEY="your-elevenlabs-key"
export GEMINI_API_KEY="your-gemini-key"
# Run the deployment script
./scripts/cloud-run/deploy.shScript Output: The script will output the final hosted URL at the end:
=== HOSTED URL FOR DEVPOST SUBMISSION ===
HOSTED_URL=https://lessonarcade-xxxxx.a.run.app
===========================================
Quick retrieval: pnpm hosted:url
Use this URL for your Devpost submission. After deployment, you can quickly retrieve the URL anytime with pnpm hosted:url without committing it to git.
If you prefer to deploy manually:
# Set variables
export GCP_PROJECT_ID="your-project-id"
export GCP_REGION="us-central1"
export AR_REPO="lessonarcade"
export CLOUD_RUN_SERVICE="lessonarcade"
export IMAGE_TAG="latest"
# Build the Docker image
docker build -t $GCP_REGION-docker.pkg.dev/$GCP_PROJECT_ID/$AR_REPO/$CLOUD_RUN_SERVICE:$IMAGE_TAG .
# Push to Artifact Registry
docker push $GCP_REGION-docker.pkg.dev/$GCP_PROJECT_ID/$AR_REPO/$CLOUD_RUN_SERVICE:$IMAGE_TAG
# Deploy to Cloud Run with environment variables
gcloud run deploy $CLOUD_RUN_SERVICE \
--image=$GCP_REGION-docker.pkg.dev/$GCP_PROJECT_ID/$AR_REPO/$CLOUD_RUN_SERVICE:$IMAGE_TAG \
--region=$GCP_REGION \
--platform=managed \
--allow-unauthenticated \
--memory=1Gi \
--cpu=1 \
--min-instances=0 \
--max-instances=10 \
--concurrency=20 \
--set-env-vars=STUDIO_BASIC_AUTH_USER=admin,STUDIO_BASIC_AUTH_PASS=secure-password,LOGGING_SALT=random-salt-string,GCP_PROJECT_ID=$GCP_PROJECT_ID,GCP_REGION=us-central1,GCP_VERTEX_MODEL=gemini-2.0-flash-expThe project includes a smoke test script to verify your deployment:
# Make the script executable (if not already)
chmod +x scripts/cloud-run/smoke-test.sh
# Get the service URL from deployment or:
SERVICE_URL=$(gcloud run services describe lessonarcade --region=us-central1 --format="value(status.url)")
# Run smoke tests
./scripts/cloud-run/smoke-test.sh $SERVICE_URL
# Or with auth credentials
STUDIO_BASIC_AUTH_USER=admin STUDIO_BASIC_AUTH_PASS=secure-password ./scripts/cloud-run/smoke-test.sh $SERVICE_URLSmoke Test Coverage:
- GET /demo (public demo page) - expects 200
- GET /demo/voice/effective-meetings (voice lesson) - expects 200
- GET /demo/voice-chat/effective-meetings (voice chat) - expects 200
- GET /studio (without auth) - expects 401
- GET /studio/voice-analytics (without auth) - expects 401
- GET /studio (with auth, if credentials provided) - expects 200
# Get service URL
SERVICE_URL=$(gcloud run services describe lessonarcade \
--region=us-central1 \
--format="value(status.url)")
echo "Service URL: $SERVICE_URL"# Test the demo page (should return 200)
curl -I $SERVICE_URL/demo
# Test a specific voice lesson
curl -I $SERVICE_URL/demo/voice/effective-meetings
# Test the Vertex AI API endpoint (GET for config check)
curl $SERVICE_URL/api/ai/gemini
# Test the studio endpoint (should return 401 without auth)
curl -I $SERVICE_URL/studio
# Test with authentication
curl -I -u admin:secure-password $SERVICE_URL/studioOpen the following URLs in your browser:
- Demo page:
https://YOUR_SERVICE_URL/demo - Voice lesson:
https://YOUR_SERVICE_URL/demo/voice/effective-meetings - Studio (requires auth):
https://YOUR_SERVICE_URL/studio - Vertex AI API (GET):
https://YOUR_SERVICE_URL/api/ai/gemini
Cloud Run automatically injects a PORT environment variable into your container. Your application must:
- Listen on the port specified by the
PORTenvironment variable - NOT hardcode a specific port
The project's Dockerfile is already configured correctly:
# Cloud Run will override with PORT env var
EXPOSE 8080
ENV PORT=8080
ENV HOSTNAME=0.0.0.0Next.js automatically reads the PORT environment variable, so no code changes are needed.
Cloud Run automatically configures a default TCP startup probe with these values:
timeoutSeconds: 240periodSeconds: 240failureThreshold: 1
This means Cloud Run will wait up to 240 seconds for your container to start accepting connections on the configured port.
For most Next.js applications, the default TCP probe is sufficient. If you need custom health checks, you can configure them:
# Example: Configure HTTP startup probe
gcloud run deploy lessonarcade \
--image=$IMAGE_URL \
--region=$REGION \
--startup-probe httpGet.path=/,httpGet.port=8080,initialDelaySeconds=0,failureThreshold=3,timeoutSeconds=1,periodSeconds=10Symptom: Service fails to start with "Container failed to start" or "Health check failed"
Cause: Application is not listening on the correct port.
Solution:
- Ensure your application reads the
PORTenvironment variable - Do not hardcode a specific port in your application
- Verify the Dockerfile sets
ENV PORT=8080(Cloud Run will override this)
Check logs:
gcloud logs tail "projects/$PROJECT_ID/logs/run.googleapis.com%2Fstdout" \
--limit=50 \
--filter="resource.labels.service_name=lessonarcade"Symptom: Service starts but then crashes repeatedly
Cause: Health check probe is failing.
Solutions:
-
Increase timeout for slow-starting applications:
gcloud run deploy lessonarcade \ --image=$IMAGE_URL \ --region=$REGION \ --startup-probe tcpSocket.port=8080,initialDelaySeconds=30,failureThreshold=3,timeoutSeconds=10,periodSeconds=10 -
Configure HTTP health check endpoint (if using custom health checks):
gcloud run deploy lessonarcade \ --image=$IMAGE_URL \ --region=$REGION \ --startup-probe httpGet.path=/health,httpGet.port=8080 -
Check application logs for startup errors:
gcloud logs tail "projects/$PROJECT_ID/logs/run.googleapis.com%2Fstdout" \ --limit=100 \ --filter="resource.labels.service_name=lessonarcade"
Symptom: Application starts but fails at runtime with "undefined" or "missing API key" errors
Cause: Required environment variables or secrets are not configured.
Solution:
-
List current environment variables:
gcloud run services describe lessonarcade \ --region=$REGION \ --format="value(spec.template.spec.containers[0].env)" -
Add missing environment variables:
gcloud run services update lessonarcade \ --region=$REGION \ --update-env-vars=KEY1=VALUE1,KEY2=VALUE2 -
Add missing secrets:
gcloud run services update lessonarcade \ --region=$REGION \ --update-secrets=SECRET_NAME=secret-name:latest -
Verify secret access:
# Check if service account has access to secrets PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') SERVICE_ACCOUNT="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" gcloud secrets get-iam-policy SECRET_NAME
Symptom: API calls to Vertex AI fail with "Permission denied" or "Access denied" errors
Cause: Cloud Run service account does not have the necessary IAM permissions.
Solution:
-
Verify service account has Vertex AI User role:
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') SERVICE_ACCOUNT="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" gcloud projects get-iam-policy $PROJECT_ID \ --filter="serviceAccount:$SERVICE_ACCOUNT" \ --format="table(bindings.role)"
-
Grant Vertex AI User role if missing:
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member="serviceAccount:$SERVICE_ACCOUNT" \ --role="roles/aiplatform.user"
-
Check Vertex AI API is enabled:
gcloud services list --enabled | grep aiplatform -
Enable Vertex AI API if needed:
gcloud services enable aiplatform.googleapis.com
Symptom: Docker build fails during deployment
Cause: Issues with dependencies or build configuration.
Solutions:
-
Check Node.js version compatibility (Next.js 16 requires Node 20.9+):
node --version
-
Verify pnpm-lock.yaml is present:
ls -la pnpm-lock.yaml
-
Build locally to debug:
docker build -t test-build . -
Check build logs:
gcloud builds list --limit=10 gcloud builds log BUILD_ID
Symptom: "Permission denied" or "Access denied" errors
Cause: Insufficient IAM permissions.
Solution:
-
Verify your IAM roles:
gcloud projects get-iam-policy $PROJECT_ID --filter="user:YOUR_EMAIL"
-
Grant required roles:
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member="user:YOUR_EMAIL" \ --role="roles/run.admin"
To remove the deployed service and resources:
# Delete the Cloud Run service
gcloud run services delete lessonarcade --region=$REGION
# Delete the Artifact Registry repository (optional)
gcloud artifacts repositories delete $AR_REPO --location=$REGION
# Delete secrets (if using Secret Manager)
gcloud secrets delete gemini-api-key
gcloud secrets delete elevenlabs-api-key
gcloud secrets delete studio-auth-user
gcloud secrets delete studio-auth-pass
gcloud secrets delete logging-salt