Overview
This guide covers a complete, production-ready deployment of CrewAI Enterprise on Google Kubernetes Engine with the following stack:
- GKE with Workload Identity (no static credentials)
- Cloud SQL for PostgreSQL 16 via Cloud SQL Auth Proxy
- GCS for object storage with IAM-signed URLs
- Artifact Registry for crew container images
- NGINX Ingress for TLS termination
- WorkOS AuthKit for SSO
- Wharf for OTLP trace collection (enabled by default)
- Studio V2 (post-install configuration — no Helm values exist for this)
Work through each section in order. The Helm install cannot succeed until all infrastructure and auth prerequisites are complete.
Prerequisites Checklist
Complete every item before running helm install.
GCP Infrastructure
Cloud SQL
GCS
Artifact Registry
WorkOS
DNS / TLS
Step 1: Enable Required GCP APIs
gcloud services enable \
container.googleapis.com \
sqladmin.googleapis.com \
storage-api.googleapis.com \
artifactregistry.googleapis.com \
iam.googleapis.com
Step 2: Create the GCP Service Account
All CrewAI workloads share a single GSA mapped to the Kubernetes ServiceAccount via Workload Identity.
export GCP_PROJECT_ID="<YOUR_PROJECT_ID>"
export GCP_REGION="<YOUR_REGION>" # e.g., us-central1
export SQL_INSTANCE="<YOUR_SQL_INSTANCE>" # e.g., crewai-db
export GSA_NAME="crewai-platform"
export GKE_NAMESPACE="crewai"
export KSA_NAME="crewai-sa" # chart default when release name is "crewai-platform"
gcloud iam service-accounts create $GSA_NAME \
--display-name="CrewAI Platform" \
--project=$GCP_PROJECT_ID
The Workload Identity binding in Step 4 references KSA_NAME. The chart creates a Kubernetes ServiceAccount named {release-name}-sa. This guide uses helm install crewai-platform, which produces crewai-platform-sa. If your release name differs, update KSA_NAME to match, or pin serviceAccount.name: crewai-platform-sa in your Helm values.
Step 3: Grant IAM Roles
# GCS object storage read/write
gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
--member="serviceAccount:${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/storage.objectAdmin"
# Signed URL generation via IAM signBlob (required for Workload Identity)
gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
--member="serviceAccount:${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/iam.serviceAccountTokenCreator"
# Artifact Registry — push/pull crew images
gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
--member="serviceAccount:${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/artifactregistry.writer"
# Cloud SQL Auth Proxy connection
gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
--member="serviceAccount:${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/cloudsql.client"
# Cloud SQL IAM database authentication
gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
--member="serviceAccount:${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/cloudsql.instanceUser"
# Node pool compute SA — crew pod image pulls from Artifact Registry
GCE_SA="$(gcloud projects describe $GCP_PROJECT_ID --format='value(projectNumber)')-compute@developer.gserviceaccount.com"
gcloud projects add-iam-policy-binding $GCP_PROJECT_ID \
--member="serviceAccount:${GCE_SA}" \
--role="roles/artifactregistry.reader"
Step 4: Bind Workload Identity
# Platform namespace — web, worker, buildkit daemon
gcloud iam service-accounts add-iam-policy-binding \
${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:${GCP_PROJECT_ID}.svc.id.goog[${GKE_NAMESPACE}/${KSA_NAME}]"
# Crews namespace — build pods that push images to Artifact Registry
gcloud iam service-accounts add-iam-policy-binding \
${GSA_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:${GCP_PROJECT_ID}.svc.id.goog[crewai-crews/default]"
The [NAMESPACE/KSA_NAME] strings must exactly match the Kubernetes namespace and ServiceAccount name. A typo here causes a silent Workload Identity failure — pods start normally but all GCP API calls return 403.
Step 5: Create Cloud SQL Databases
Enable IAM authentication on the instance, then create all four required databases.
# Enable IAM authentication (off by default — required for autoIamAuthn)
gcloud sql instances patch $SQL_INSTANCE \
--database-flags=cloudsql.iam_authentication=on
# Create all four databases
gcloud sql databases create crewai_plus_production --instance=$SQL_INSTANCE
gcloud sql databases create crewai_plus_cable_production --instance=$SQL_INSTANCE
gcloud sql databases create crewai_plus_oauth_production --instance=$SQL_INSTANCE
gcloud sql databases create wharf --instance=$SQL_INSTANCE
crewai_plus_oauth_production must be created explicitly. The chart default for POSTGRES_OAUTH_DB is oauth_db — if you do not override it in envVars, the OAuth service attempts to connect to a database that does not exist and fails silently at startup.Similarly, wharf must be pre-created. The Wharf trace collector expects this database to exist on startup.
Create the IAM Database User
gcloud sql users create ${GSA_NAME}@${GCP_PROJECT_ID}.iam \
--instance=$SQL_INSTANCE \
--type=CLOUD_IAM_SERVICE_ACCOUNT
Grant Privileges
Connect to the Cloud SQL instance and grant the IAM user access to all four databases. Replace GSA_NAME@GCP_PROJECT_ID.iam with your actual value (e.g., crewai-platform@my-project.iam):
gcloud sql connect $SQL_INSTANCE --user=postgres --database=postgres
-- Database-level access
GRANT ALL PRIVILEGES ON DATABASE crewai_plus_production TO "crewai-platform@<PROJECT>.iam";
GRANT ALL PRIVILEGES ON DATABASE crewai_plus_cable_production TO "crewai-platform@<PROJECT>.iam";
GRANT ALL PRIVILEGES ON DATABASE crewai_plus_oauth_production TO "crewai-platform@<PROJECT>.iam";
GRANT ALL PRIVILEGES ON DATABASE wharf TO "crewai-platform@<PROJECT>.iam";
-- Schema/table/sequence access — required for Rails migrations
\c crewai_plus_production
GRANT ALL ON SCHEMA public TO "crewai-platform@<PROJECT>.iam";
GRANT ALL ON ALL TABLES IN SCHEMA public TO "crewai-platform@<PROJECT>.iam";
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO "crewai-platform@<PROJECT>.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "crewai-platform@<PROJECT>.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "crewai-platform@<PROJECT>.iam";
\c crewai_plus_cable_production
GRANT ALL ON SCHEMA public TO "crewai-platform@<PROJECT>.iam";
GRANT ALL ON ALL TABLES IN SCHEMA public TO "crewai-platform@<PROJECT>.iam";
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO "crewai-platform@<PROJECT>.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "crewai-platform@<PROJECT>.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "crewai-platform@<PROJECT>.iam";
\c crewai_plus_oauth_production
GRANT ALL ON SCHEMA public TO "crewai-platform@<PROJECT>.iam";
GRANT ALL ON ALL TABLES IN SCHEMA public TO "crewai-platform@<PROJECT>.iam";
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO "crewai-platform@<PROJECT>.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "crewai-platform@<PROJECT>.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "crewai-platform@<PROJECT>.iam";
\c wharf
GRANT ALL ON SCHEMA public TO "crewai-platform@<PROJECT>.iam";
GRANT ALL ON ALL TABLES IN SCHEMA public TO "crewai-platform@<PROJECT>.iam";
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO "crewai-platform@<PROJECT>.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "crewai-platform@<PROJECT>.iam";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "crewai-platform@<PROJECT>.iam";
Step 6: Create GCS Bucket
export GCS_BUCKET="<YOUR_BUCKET_NAME>" # e.g., crewai-prod-storage
gsutil mb -p $GCP_PROJECT_ID -l $GCP_REGION -b on gs://$GCS_BUCKET
gsutil versioning set on gs://$GCS_BUCKET
Step 7: Create Artifact Registry Repository
The repository must have mutable tags. CrewAI overwrites image tags on each crew deployment. Immutable tag repositories cause push failures.
export AR_REPO="crewai"
gcloud artifacts repositories create $AR_REPO \
--repository-format=docker \
--location=$GCP_REGION \
--description="CrewAI crew images"
# Confirm the registry prefix you will use in CREW_IMAGE_REGISTRY_OVERRIDE:
echo "${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/${AR_REPO}"
The CREW_IMAGE_REGISTRY_OVERRIDE value is this prefix without the /crewai-enterprise suffix — the platform appends that automatically.
WORKOS_API_KEY and WORKOS_COOKIE_PASSWORD must be placed under envVars:, not secrets:. This is a known chart limitation. Values placed under secrets: are silently absent from pods — WorkOS authentication fails with no clear error message.After install, verify both variables are present in the pod:kubectl exec -it deploy/crewai-web -- env | grep -E 'WORKOS_API_KEY|WORKOS_COOKIE_PASSWORD'
- In the WorkOS Dashboard, navigate to Applications → Create Application (name it e.g.,
CrewAI Platform)
- Add redirect URI:
https://<YOUR_DOMAIN>/auth/workos/callback
- Note your Client ID from the application main page
- Navigate to Developer → Domains and note your AuthKit domain
- Navigate to the main API Keys page and copy your API key (starts with
sk_live_)
- Generate a cookie password (must be 32 characters or fewer):
openssl rand -base64 32 | cut -c -32
The | cut -c -32 truncation is mandatory. WorkOS silently rejects cookie passwords longer than 32 characters — the install succeeds but all authentication attempts fail.
Complete values.yaml
Replace all <PLACEHOLDER> values before running helm install.
# ============================================================
# CrewAI Enterprise — GCP + WorkOS + Wharf + Studio V2
# ============================================================
# Workload Identity: maps the Kubernetes ServiceAccount to the GCP Service Account
serviceAccount:
annotations:
iam.gke.io/gcp-service-account: crewai-platform@<YOUR_PROJECT_ID>.iam.gserviceaccount.com
rbac:
create: true # creates the crewai-platform-sa Kubernetes ServiceAccount
# Namespace for crew workloads
crewNamespace: "crewai-crews"
# Image pull credentials for platform images from images.crewai.com (Replicated proxy)
# Required for direct Helm installs; not needed for KOTS installs
image:
registries:
- host: "images.crewai.com"
username: "<YOUR_LICENSE_EMAIL>"
password: "<YOUR_REPLICATED_LICENSE_TOKEN>"
# ─────────────────────────────────────────
# Disable bundled services — use GCP managed
# ─────────────────────────────────────────
postgres:
enabled: false
minio:
enabled: false
# ─────────────────────────────────────────
# Cloud SQL Auth Proxy
# ─────────────────────────────────────────
cloudSqlProxy:
enabled: true
instanceConnectionName: "<YOUR_PROJECT_ID>:<YOUR_REGION>:<YOUR_SQL_INSTANCE>" # e.g., my-project:us-central1:crewai-db
port: 5432
privateIp: true # use private IP (recommended for VPC-peered instances)
autoIamAuthn: true # IAM-based DB auth via Workload Identity — no DB_PASSWORD needed
# ─────────────────────────────────────────
# Environment variables
# ─────────────────────────────────────────
envVars:
# Application
APPLICATION_HOST: "<YOUR_DOMAIN>" # e.g., crewai.company.com
RAILS_LOG_LEVEL: "info"
# Database (via Cloud SQL Auth Proxy listening on 127.0.0.1)
DB_HOST: "127.0.0.1"
DB_PORT: "5432"
DB_USER: "crewai-platform@<YOUR_PROJECT_ID>.iam" # GSA email WITHOUT .gserviceaccount.com suffix
POSTGRES_DB: "crewai_plus_production"
POSTGRES_CABLE_DB: "crewai_plus_cable_production"
POSTGRES_OAUTH_DB: "crewai_plus_oauth_production" # MUST override — chart default is "oauth_db"
# GCS object storage
STORAGE_SERVICE: "google"
GCS_BUCKET: "<YOUR_BUCKET_NAME>"
GCS_REGION: "<YOUR_REGION>"
GCS_IAM_SIGNING: "true" # REQUIRED for Workload Identity — omitting causes Google::Cloud::Storage::SignedUrlUnavailable at runtime
# Artifact Registry for crew images
# Platform appends /crewai-enterprise automatically — do NOT include it here
CREW_IMAGE_REGISTRY_OVERRIDE: "<YOUR_REGION>-docker.pkg.dev/<YOUR_PROJECT_ID>/crewai"
# WorkOS SSO
# CRITICAL: these MUST be under envVars — NOT under secrets
# Values under secrets: are silently absent from pods (chart bug)
AUTH_PROVIDER: "workos"
WORKOS_CLIENT_ID: "<YOUR_WORKOS_CLIENT_ID>" # From WorkOS Application main page
WORKOS_AUTHKIT_DOMAIN: "<YOUR_ORG>.authkit.app" # From WorkOS Developer → Domains
WORKOS_API_KEY: "<YOUR_WORKOS_API_KEY>" # Starts with sk_live_ — must be under envVars
WORKOS_COOKIE_PASSWORD: "<YOUR_32_CHAR_PASSWORD>" # Max 32 chars — must be under envVars
# ─────────────────────────────────────────
# Secrets (sensitive values rendered as Kubernetes Secrets)
# ─────────────────────────────────────────
secrets:
SECRET_KEY_BASE: "<YOUR_SECRET_KEY_BASE>" # generate: openssl rand -hex 64
# DB_PASSWORD intentionally omitted — not needed with autoIamAuthn: true
# ─────────────────────────────────────────
# Wharf (OTLP trace collection)
# ─────────────────────────────────────────
wharf:
enabled: true # true is the chart default; included here for explicitness
# Wharf uses the same DB_HOST/DB_PORT/DB_USER as the main app (via Cloud SQL Auth Proxy)
# postgres.wharfDatabase defaults to "wharf" — matches the database you created above
# ─────────────────────────────────────────
# Ingress (NGINX)
# ─────────────────────────────────────────
web:
enableSslFromPuma: false # MUST be false when NGINX handles TLS termination
replicaCount: 2
resources:
requests:
cpu: "1000m"
memory: "6Gi"
limits:
cpu: "6"
memory: "12Gi"
ingress:
enabled: true
className: "nginx"
host: "<YOUR_DOMAIN>"
nginx:
tls:
enabled: true
secretName: "crewai-tls" # Kubernetes TLS secret in the crewai namespace
# ─────────────────────────────────────────
# Worker
# ─────────────────────────────────────────
worker:
replicaCount: 2
resources:
requests:
cpu: "1000m"
memory: "6Gi"
limits:
cpu: "6"
memory: "12Gi"
# ─────────────────────────────────────────
# BuildKit (crew image builds)
# ─────────────────────────────────────────
buildkit:
enabled: true
replicaCount: 1
resources:
requests:
cpu: "500m"
memory: "2Gi"
limits:
cpu: "4"
memory: "8Gi"
# ─────────────────────────────────────────
# OAuth service (Built-in Integrations)
# Required for Google Workspace, Microsoft 365, HubSpot, etc.
# Not required for WorkOS authentication itself
# ─────────────────────────────────────────
oauth:
enabled: true
ingress:
enabled: true
className: "nginx"
host: "<YOUR_DOMAIN>"
pathPrefix: "/oauthsvc"
Step 9: Install
helm install crewai-platform \
oci://registry.crewai.com/crewai/stable/crewai-platform \
--values values.yaml \
--namespace crewai \
--create-namespace
Watch pods come up:
kubectl get pods -n crewai -w
All pods should reach Running within a few minutes. If any pod shows CrashLoopBackOff or Error, check logs before proceeding:
kubectl logs -n crewai deploy/crewai-web --tail=50
Step 10: Post-Install Annotations
After the first install, the crewai-crews namespace is created automatically. Annotate its default ServiceAccount so build pods can authenticate to Artifact Registry via Workload Identity:
kubectl annotate serviceaccount default -n crewai-crews --overwrite \
iam.gke.io/gcp-service-account=crewai-platform@<YOUR_PROJECT_ID>.iam.gserviceaccount.com
Without this annotation, all crew image builds fail with a permission denied error when pushing to Artifact Registry.
Step 11: Required Post-Install Commands
Run these commands in order. Each must complete before proceeding to the next.
# Initialize the internal organization (required before any other setup)
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails studio:install_internal_organization
# Set up default roles and permissions
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails factory:setup_permissions_defaults
# Add the first owner (replace with your actual admin email)
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails 'factory:add_owner[2,admin@company.com]'
# Grant admin access for WorkOS — run AFTER the user has logged in at least once via WorkOS SSO
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails 'factory:grant_admin[admin@company.com]'
factory:grant_admin is the correct command for WorkOS SSO. This is distinct from Entra ID deployments, which use Azure App Roles instead.factory:grant_admin will fail if the user has not yet authenticated through WorkOS. Have the admin log in via the web UI first, then run the command.
Step 12: Enable Studio V2
Studio V2 cannot be configured in values.yaml. Adding studioV2.enabled, STUDIO_V2_ENABLED, or any similar key has no effect — Helm silently ignores unrecognized keys. Setup is always post-install via the UI and kubectl.
12a: Create the LLM Connection (UI)
- Navigate to Settings → LLM Connections
- Click New Connection
- Set the name to exactly
studio-v2 (lowercase, no spaces — the install commands in 12c look up this name specifically)
- Select your LLM provider, enter the model name and API key
- Click Save
12b: Set as Default Connection (UI)
- Navigate to Settings → Crew Studio
- Under Default Connection, select
studio-v2
- Click Save
12c: Run Install Commands
Run these in order. Each must complete successfully before running the next.
# Install the Studio agent
# Fails if the studio-v2 LLM Connection does not exist yet — complete 12a first
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails studio:agent:install
# Sync CrewAI and enterprise tools
kubectl exec -it deploy/crewai-web -n crewai -- \
bin/rails studio:tools:sync_crewai_tools studio:tools:sync_enterprise_tools
# Install the Studio runner
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails studio:runner:install
Step 13: Verify
# All pods running
kubectl get pods -n crewai
# Web pod responding
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails runner "puts 'ok'"
WorkOS authentication
Navigate to https://<YOUR_DOMAIN>. You should be redirected to the WorkOS AuthKit login page.
If authentication fails, check that both WorkOS keys are present in the pod:
kubectl exec -it deploy/crewai-web -n crewai -- env | grep -E 'WORKOS_API_KEY|WORKOS_COOKIE_PASSWORD'
If either variable is missing, confirm they are under envVars: in values.yaml — not secrets: — and run helm upgrade.
WorkOS redirect URI
If you see a “redirect_uri_mismatch” error in the WorkOS dashboard, confirm the redirect URI in WorkOS exactly matches:
https://<YOUR_DOMAIN>/auth/workos/callback
GCS signed URLs
If file uploads or downloads fail with SignedUrlUnavailable, confirm:
envVars:
GCS_IAM_SIGNING: "true"
And that the GSA has roles/iam.serviceAccountTokenCreator.
Wharf trace collection
kubectl get pods -n crewai | grep wharf
kubectl logs -n crewai deploy/crewai-wharf --tail=20
Studio V2
kubectl get deploy -n crewai | grep studio
kubectl logs -n crewai -l app=studio-assistant --tail=20
Both studio-assistant and studio-runner deployments should show Running.
Troubleshooting
WorkOS auth fails with no clear error
Both WORKOS_API_KEY and WORKOS_COOKIE_PASSWORD are absent from the pod. Move them from secrets: to envVars: and run helm upgrade.
WORKOS_COOKIE_PASSWORD rejected silently
The password exceeds 32 characters. Regenerate: openssl rand -base64 32 | cut -c -32.
GCS signed URLs fail at runtime (install succeeds)
GCS_IAM_SIGNING is missing from envVars. This error only appears when users try to access uploaded files — the install itself succeeds, making it easy to miss. Add GCS_IAM_SIGNING: "true" and run helm upgrade.
OAuth service connects to wrong database
POSTGRES_OAUTH_DB is not set in envVars. The chart default oauth_db does not match the crewai_plus_oauth_production database you created. Add the override explicitly.
Cloud SQL connection refused on 127.0.0.1
The Cloud SQL Auth Proxy sidecar is not running. Check:
kubectl logs deploy/crewai-web -n crewai -c cloud-sql-proxy
Verify cloudSqlProxy.instanceConnectionName matches the output of:
gcloud sql instances describe $SQL_INSTANCE --format='value(connectionName)'
IAM database auth fails
Check that:
cloudsql.iam_authentication=on flag is set on the instance
DB_USER is the GSA email without .gserviceaccount.com (e.g., crewai-platform@my-project.iam)
- The IAM database user exists:
gcloud sql users list --instance=$SQL_INSTANCE
502 errors from NGINX
web.enableSslFromPuma is not set to false. When NGINX handles TLS termination, Puma must not also attempt SSL. Set enableSslFromPuma: false in your web: block and run helm upgrade.
Crew image pushes fail (unauthorized)
The default ServiceAccount in crewai-crews is missing the Workload Identity annotation. Run:
kubectl annotate serviceaccount default -n crewai-crews --overwrite \
iam.gke.io/gcp-service-account=crewai-platform@<YOUR_PROJECT_ID>.iam.gserviceaccount.com
studio:agent:install fails
The studio-v2 LLM Connection does not exist yet. Complete Steps 12a and 12b in the UI, then re-run the command.