terrylica

infra-deploy

Cal.com self-hosted deployment to GCP Cloud Run with Supabase PostgreSQL. Docker Compose for local dev.

terrylica 61 9 Updated 4mo ago

Resources

1
GitHub

Install

npx skillscat add terrylica/cc-skills/infra-deploy

Install via the SkillsCat registry.

About this skill

This skill automates the deployment of Cal.com self‑hosted to Google Cloud Run using Supabase PostgreSQL, while also supporting local development with Docker Compose. It verifies GCP project and authentication settings, Supabase configuration, required secrets, and API enablement before building and pushing the container.

SKILL.md

Infrastructure Deployment

Deploy Cal.com self-hosted to GCP Cloud Run with Supabase PostgreSQL, or run locally via Docker Compose.

Self-Evolving Skill: This skill improves through use. If instructions are wrong, parameters drifted, or a workaround was needed — fix this file immediately, don't defer. Only update for real, reproducible issues.

Mandatory Preflight

Step 1: Check GCP Project Configuration

echo "CALCOM_GCP_PROJECT: ${CALCOM_GCP_PROJECT:-NOT_SET}"
echo "CALCOM_GCP_ACCOUNT: ${CALCOM_GCP_ACCOUNT:-NOT_SET}"
echo "CALCOM_GCP_REGION: ${CALCOM_GCP_REGION:-us-central1}"

If NOT_SET: These must be configured in .mise.local.toml. Run the setup command.

Step 2: Verify GCP Authentication

gcloud auth list --filter="status=ACTIVE" --format="value(account)" 2>/dev/null

Step 3: Check Supabase Configuration

echo "SUPABASE_PROJECT_REF: ${SUPABASE_PROJECT_REF:-NOT_SET}"
echo "SUPABASE_DB_URL_REF: ${SUPABASE_DB_URL_REF:-NOT_SET}"

Step 4: Verify Cal.com Secrets

echo "CALCOM_NEXTAUTH_SECRET_REF: ${CALCOM_NEXTAUTH_SECRET_REF:-NOT_SET}"
echo "CALCOM_ENCRYPTION_KEY_REF: ${CALCOM_ENCRYPTION_KEY_REF:-NOT_SET}"
echo "CALCOM_CRON_API_KEY_REF: ${CALCOM_CRON_API_KEY_REF:-NOT_SET}"

All 1Password references must be SET. Secrets are stored in Claude Automation vault.


Deploy Target: GCP Cloud Run

Step 1: Verify GCP APIs Enabled

gcloud services list --enabled \
  --project="$CALCOM_GCP_PROJECT" \
  --account="$CALCOM_GCP_ACCOUNT" 2>/dev/null | grep -E "run|artifact|build"

Required APIs: Cloud Run, Artifact Registry, Cloud Build.

Step 2: Build Container

# From the cal.com fork directory
cd ~/fork-tools/cal.com

# Build Docker image
docker build -t calcom-self-hosted .

Step 3: Push to Artifact Registry

# Tag for Artifact Registry
docker tag calcom-self-hosted \
  "${CALCOM_GCP_REGION}-docker.pkg.dev/${CALCOM_GCP_PROJECT}/calcom/calcom:latest"

# Push
docker push \
  "${CALCOM_GCP_REGION}-docker.pkg.dev/${CALCOM_GCP_PROJECT}/calcom/calcom:latest"

Step 4: Deploy to Cloud Run

# Resolve secrets from 1Password
NEXTAUTH_SECRET=$(op read "$CALCOM_NEXTAUTH_SECRET_REF")
ENCRYPTION_KEY=$(op read "$CALCOM_ENCRYPTION_KEY_REF")
CRON_API_KEY=$(op read "$CALCOM_CRON_API_KEY_REF")
DATABASE_URL=$(op read "$SUPABASE_DB_URL_REF")

gcloud run deploy calcom \
  --image="${CALCOM_GCP_REGION}-docker.pkg.dev/${CALCOM_GCP_PROJECT}/calcom/calcom:latest" \
  --region="$CALCOM_GCP_REGION" \
  --project="$CALCOM_GCP_PROJECT" \
  --account="$CALCOM_GCP_ACCOUNT" \
  --platform=managed \
  --allow-unauthenticated \
  --port=3000 \
  --memory=512Mi \
  --cpu=1 \
  --min-instances=0 \
  --max-instances=2 \
  --set-env-vars="DATABASE_URL=${DATABASE_URL}" \
  --set-env-vars="NEXTAUTH_SECRET=${NEXTAUTH_SECRET}" \
  --set-env-vars="CALENDSO_ENCRYPTION_KEY=${ENCRYPTION_KEY}" \
  --set-env-vars="CRON_API_KEY=${CRON_API_KEY}" \
  --set-env-vars="NEXT_PUBLIC_WEBAPP_URL=https://calcom-${CALCOM_GCP_PROJECT}.run.app" \
  --set-env-vars="NEXT_PUBLIC_API_V2_URL=https://calcom-${CALCOM_GCP_PROJECT}.run.app/api/v2"

Deploy Target: Docker Compose (Local Dev)

docker-compose.yml Template

version: "3.9"
services:
  calcom:
    image: calcom/cal.com:latest
    restart: unless-stopped
    ports:
      - "3000:3000"
    env_file:
      - .env
    depends_on:
      database:
        condition: service_healthy

  database:
    image: postgres:15
    restart: unless-stopped
    volumes:
      - calcom-db:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: calcom
      POSTGRES_PASSWORD: calcom
      POSTGRES_DB: calcom
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U calcom"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  calcom-db:

Local .env Template

DATABASE_URL=postgresql://calcom:calcom@database:5432/calcom
NEXTAUTH_SECRET=<generate-with-openssl>
CALENDSO_ENCRYPTION_KEY=<generate-with-openssl>
CRON_API_KEY=<generate-with-openssl>
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
NEXT_PUBLIC_API_V2_URL=http://localhost:3000/api/v2

Deploy Target: Webhook Relay (Pushover Notifications)

Lightweight Cloud Run service bridging Cal.com webhooks to Pushover emergency alerts. Zero dependencies (Python stdlib only).

Step 1: Verify Pushover Credentials

echo "PUSHOVER_APP_TOKEN: ${PUSHOVER_APP_TOKEN:+SET}"
echo "PUSHOVER_USER_KEY: ${PUSHOVER_USER_KEY:+SET}"
echo "PUSHOVER_SOUND: ${PUSHOVER_SOUND:-dune}"

All must be SET. See pushover-setup.md for credential setup.

Step 2: Deploy Webhook Relay to Cloud Run

RELAY_SOURCE="$HOME/.claude/plugins/marketplaces/cc-skills/plugins/calcom-commander/scripts/webhook-relay"

gcloud run deploy calcom-pushover-webhook \
  --source "$RELAY_SOURCE" \
  --project="$CALCOM_GCP_PROJECT" \
  --account="$CALCOM_GCP_ACCOUNT" \
  --region="$CALCOM_GCP_REGION" \
  --platform managed \
  --allow-unauthenticated \
  --set-env-vars="PUSHOVER_TOKEN=$PUSHOVER_APP_TOKEN,PUSHOVER_USER=$PUSHOVER_USER_KEY,PUSHOVER_SOUND=${PUSHOVER_SOUND:-dune}" \
  --memory=128Mi \
  --cpu=1 \
  --min-instances=0 \
  --max-instances=1 \
  --timeout=30 \
  --quiet

Note the Service URL from the output. Store in .mise.local.toml as WEBHOOK_RELAY_URL.

Step 3: Verify Health

curl -s "$WEBHOOK_RELAY_URL" | python3 -m json.tool
# Expected: {"status": "healthy", "service": "calcom-pushover-webhook"}

Step 4: Register Cal.com Webhook

CALCOM_API_KEY=$(op item get "$CALCOM_OP_UUID" --vault "Claude Automation" --fields password --reveal)

curl -s -X POST "https://api.cal.com/v1/webhooks?apiKey=$CALCOM_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"subscriberUrl\":\"$WEBHOOK_RELAY_URL\",\"eventTriggers\":[\"BOOKING_CREATED\",\"BOOKING_RESCHEDULED\",\"BOOKING_CANCELLED\"],\"active\":true}"

Step 5: Test with Simulated Booking

curl -s -X POST "$WEBHOOK_RELAY_URL" \
  -H "Content-Type: application/json" \
  -d '{"triggerEvent":"BOOKING_CREATED","payload":{"title":"Test Meeting","attendees":[{"name":"Test User"}],"startTime":"2026-01-01T10:00:00Z","endTime":"2026-01-01T10:30:00Z"}}'

Expected: Pushover emergency alert with "dune" sound on your device.


Supabase Database Management

Check Connection

DATABASE_URL=$(op read "$SUPABASE_DB_URL_REF")
psql "$DATABASE_URL" -c "SELECT version();"

Run Migrations

cd ~/fork-tools/cal.com
DATABASE_URL=$(op read "$SUPABASE_DB_URL_REF") npx prisma migrate deploy

1Password Secret References

All secrets in Claude Automation vault (biometric-free access):

Secret 1Password Reference
NEXTAUTH_SECRET op://Claude Automation/<item-id>/NEXTAUTH_SECRET
CALENDSO_ENCRYPTION_KEY op://Claude Automation/<item-id>/CALENDSO_ENCRYPTION_KEY
CRON_API_KEY op://Claude Automation/<item-id>/CRON_API_KEY
DATABASE_URL op://Claude Automation/<item-id>/DATABASE_URL
DATABASE_DIRECT_URL op://Claude Automation/<item-id>/DATABASE_DIRECT_URL

Post-Change Checklist

  • YAML frontmatter valid (no colons in description)
  • Trigger keywords current
  • Path patterns use $HOME not hardcoded paths
  • 1Password references are agnostic (no hardcoded UUIDs)

Post-Execution Reflection

After this skill completes, reflect before closing the task:

  1. Locate yourself. — Find this SKILL.md's canonical path before editing.
  2. What failed? — Fix the instruction that caused it.
  3. What worked better than expected? — Promote to recommended practice.
  4. What drifted? — Fix any script, reference, or dependency that no longer matches reality.
  5. Log it. — Evolution-log entry with trigger, fix, and evidence.

Do NOT defer. The next invocation inherits whatever you leave behind.