Skip to main content

Overview

This guide covers a full production deployment of CrewAI Platform on Azure AKS using:
  • Azure Database for PostgreSQL Flexible Server (PostgreSQL 16) for all databases including Wharf
  • Azure Blob Storage for object storage
  • Azure Container Registry (ACR) for crew container images
  • NGINX Ingress Controller for ingress with cert-manager TLS termination
  • Microsoft Entra ID for SSO authentication
  • Wharf for OTLP trace and span collection
  • Studio V2 for the AI-powered crew builder (post-install)
Entra ID is the natural auth choice for Azure deployments — your users are already in Azure AD, so there is no separate identity provider to configure. This is a self-contained guide. All required configuration is included here — no cross-referencing other guides for values.

Prerequisites Checklist

Complete every item before running helm install.

Azure Infrastructure

ComponentRequirement
AKS clusterKubernetes 1.32.0+, AMD64 worker nodes
NGINX Ingress ControllerInstalled and running
cert-managerInstalled with a ClusterIssuer configured
Azure PostgreSQL Flexible ServerPostgreSQL 16, accessible from AKS worker nodes
Azure Blob StorageContainer created
Azure Container RegistryMUTABLE tags, AKS pull access granted
Managed IdentityCreated with Blob Storage Contributor role for Workload Identity
Federated credentialConfigured for the crewai-sa ServiceAccount in the crewai namespace
CrewAI Platform only supports AMD64 (x86_64) worker nodes. ARM64 worker nodes are not supported.

Microsoft Entra ID

StepStatus
App registration created in Azure portal
Redirect URI configured: https://<YOUR_DOMAIN>/auth/entra_id/callback
Application (client) ID collected
Directory (tenant) ID collected
Client secret created and value copied
Admin consent granted for Microsoft Graph User.Read
App Roles created: member and factory-admin
Admin user assigned factory-admin App Role
The redirect URI must be configured in Azure before running helm install. Authentication will fail silently if it is missing or uses the wrong path.

Tools

  • kubectl connected to your AKS cluster
  • helm 3.10+
  • Azure CLI (az) authenticated to your subscription

Infrastructure Setup

PostgreSQL: Pre-Create All Four Databases

Create the Azure PostgreSQL Flexible Server if it does not already exist:
az postgres flexible-server create \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --name crewai-postgres \
  --location <AZURE_REGION> \
  --admin-user crewai \
  --admin-password <DB_PASSWORD> \
  --sku-name Standard_D4s_v3 \
  --tier GeneralPurpose \
  --version 16 \
  --storage-size 128
Create all four required databases:
az postgres flexible-server db create \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --server-name crewai-postgres \
  --database-name crewai_plus_production

az postgres flexible-server db create \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --server-name crewai-postgres \
  --database-name crewai_plus_cable_production

az postgres flexible-server db create \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --server-name crewai-postgres \
  --database-name crewai_plus_oauth_production

az postgres flexible-server db create \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --server-name crewai-postgres \
  --database-name wharf
Then connect as the admin user and grant privileges:
-- Grant full access to the crewai user on all databases
GRANT ALL PRIVILEGES ON DATABASE crewai_plus_production TO crewai;
GRANT ALL PRIVILEGES ON DATABASE crewai_plus_cable_production TO crewai;
GRANT ALL PRIVILEGES ON DATABASE crewai_plus_oauth_production TO crewai;
GRANT ALL PRIVILEGES ON DATABASE wharf TO crewai;
The chart default for POSTGRES_OAUTH_DB is oauth_db. You must override it to crewai_plus_oauth_production to match the database you created above. A mismatch causes the OAuth service to fail on startup.
When postgres.enabled: false, the Helm chart does not create databases automatically. All four databases must exist before helm install runs.

Azure Blob Storage: Create Container

# Create a storage account
az storage account create \
  --name <YOUR_STORAGE_ACCOUNT> \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --location <AZURE_REGION> \
  --sku Standard_LRS \
  --kind StorageV2

# Create the blob container
az storage container create \
  --name <YOUR_CONTAINER_NAME> \
  --account-name <YOUR_STORAGE_ACCOUNT>

ACR: Create Repository and Grant AKS Access

# Create the registry
az acr create \
  --name <YOUR_ACR_NAME> \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --sku Standard

# Grant AKS pull access to ACR
az aks update \
  --name <YOUR_AKS_CLUSTER> \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --attach-acr <YOUR_ACR_NAME>
ACR image tags must be mutable. CrewAI overwrites tags for crew versions — immutable tags cause build failures. The default ACR configuration allows mutable tags.
CREW_IMAGE_REGISTRY_OVERRIDE must be set to <YOUR_ACR_NAME>.azurecr.io/<YOUR_ORG> — without the /crewai-enterprise suffix. The platform appends /crewai-enterprise automatically. If your ACR image path is myregistry.azurecr.io/production/crewai-enterprise, set only myregistry.azurecr.io/production.

AKS Workload Identity: Managed Identity for Blob Storage

Using AKS Workload Identity is the recommended approach. It avoids storing storage account keys in Kubernetes secrets.
# Create a Managed Identity
az identity create \
  --name crewai-workload-identity \
  --resource-group <YOUR_RESOURCE_GROUP>

# Capture the client ID and principal ID
MANAGED_IDENTITY_CLIENT_ID=$(az identity show \
  --name crewai-workload-identity \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --query clientId -o tsv)

MANAGED_IDENTITY_PRINCIPAL_ID=$(az identity show \
  --name crewai-workload-identity \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --query principalId -o tsv)

# Grant Storage Blob Data Contributor on the container
STORAGE_ACCOUNT_ID=$(az storage account show \
  --name <YOUR_STORAGE_ACCOUNT> \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --query id -o tsv)

az role assignment create \
  --assignee $MANAGED_IDENTITY_PRINCIPAL_ID \
  --role "Storage Blob Data Contributor" \
  --scope "$STORAGE_ACCOUNT_ID/blobServices/default/containers/<YOUR_CONTAINER_NAME>"

# Get the AKS OIDC issuer URL
AKS_OIDC_ISSUER=$(az aks show \
  --name <YOUR_AKS_CLUSTER> \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --query oidcIssuerProfile.issuerUrl -o tsv)

# Create federated credential — must reference namespace and service account name exactly
az identity federated-credential create \
  --name crewai-federated-credential \
  --identity-name crewai-workload-identity \
  --resource-group <YOUR_RESOURCE_GROUP> \
  --issuer $AKS_OIDC_ISSUER \
  --subject "system:serviceaccount:crewai:crewai-sa" \
  --audiences api://AzureADTokenExchange
The federated credential subject must match the ServiceAccount that the Helm chart creates. With rbac.create: true (the chart default), the chart creates a ServiceAccount named crewai-sa in the namespace you deploy into. If you deploy to a namespace other than crewai, update the subject accordingly.

NGINX Ingress and cert-manager

# Install NGINX Ingress Controller
helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx --create-namespace

# Install cert-manager
helm upgrade --install cert-manager cert-manager \
  --repo https://charts.jetstack.io \
  --namespace cert-manager --create-namespace \
  --set crds.enabled=true
Create a ClusterIssuer for Let’s Encrypt:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: <YOUR_EMAIL>
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
kubectl apply -f cluster-issuer.yaml

Entra ID: Azure Portal Setup

Step 1: App Registration

  1. Go to portal.azure.com > Microsoft Entra ID > App registrations > New registration
  2. Name: CrewAI (or your preferred name)
  3. Supported account types: Accounts in this organizational directory only
  4. Redirect URI: Web platform — https://<YOUR_DOMAIN>/auth/entra_id/callback
  5. Click Register
The redirect URI path must be exactly /auth/entra_id/callback (lowercase, underscore). Configure this before running helm install — a missing or incorrect URI causes authentication to fail with no clear error.

Step 2: Collect Credentials

From the app overview page, copy:
  • Application (client) IDENTRA_ID_CLIENT_ID
  • Directory (tenant) IDENTRA_ID_TENANT_ID

Step 3: Create Client Secret

  1. Left sidebar > Manage > Certificates & secrets
  2. New client secret — enter a description, choose expiration
  3. Copy the Value immediately — it is not shown again → ENTRA_ID_CLIENT_SECRET
  1. Enterprise applications > select your app
  2. Security > Permissions > Grant admin consent
  3. Confirm consent for Microsoft Graph User.Read

Step 5: Create App Roles

  1. Back in App registrations > your app > Manage > App roles
  2. Create two roles:
Display NameValueAllowed Member Types
MembermemberUsers/Groups
Factory Adminfactory-adminUsers/Groups
Ensure “Do you want to enable this app role?” is checked for each.

Step 6: Assign Users

  1. Enterprise applications > your app > Manage > Properties
  2. Set Assignment required? to Yes, then Save
  3. Manage > Users and groups > Add user/group
    • Regular users: assign Member role
    • Admin users: assign Factory Admin role
For Entra ID deployments, admin access is granted exclusively through the Factory Admin App Role in Azure portal. Do NOT run factory:grant_admin — it writes to a database table that is not consulted for Entra ID users. The platform reads admin status from the JWT roles claim.

Complete values.yaml

Replace all <PLACEHOLDER> values before running helm install.
values.yaml
# ──────────────────────────────────────────────
# Disable bundled PostgreSQL — using Azure PostgreSQL Flexible Server
# ──────────────────────────────────────────────
postgres:
  enabled: false

# ──────────────────────────────────────────────
# Disable bundled MinIO — using Azure Blob Storage
# ──────────────────────────────────────────────
minio:
  enabled: false

# ──────────────────────────────────────────────
# Database: Azure PostgreSQL Flexible Server
# ──────────────────────────────────────────────
envVars:
  DB_HOST: "<YOUR_SERVER_NAME>.postgres.database.azure.com"  # e.g. crewai-postgres.postgres.database.azure.com
  DB_PORT: "5432"
  DB_USER: "crewai"
  POSTGRES_DB: "crewai_plus_production"
  POSTGRES_CABLE_DB: "crewai_plus_cable_production"
  POSTGRES_OAUTH_DB: "crewai_plus_oauth_production"  # Chart default is "oauth_db" — must match what you created above

  # ──────────────────────────────────────────
  # Object storage: Azure Blob Storage
  # Using AKS Workload Identity — no static credentials needed
  # ──────────────────────────────────────────
  STORAGE_SERVICE: "microsoft"
  AZURE_STORAGE_ACCOUNT_NAME: "<YOUR_STORAGE_ACCOUNT>"   # e.g. crewaistorage
  AZURE_CONTAINER_NAME: "<YOUR_CONTAINER_NAME>"          # e.g. crewai-assets

  # ──────────────────────────────────────────
  # Crew image registry: Azure Container Registry
  # Value is the registry prefix WITHOUT /crewai-enterprise
  # The platform appends /crewai-enterprise automatically
  # ──────────────────────────────────────────
  CREW_IMAGE_REGISTRY_OVERRIDE: "<YOUR_ACR_NAME>.azurecr.io/<YOUR_ORG>"
  # Example: myregistry.azurecr.io/production
  # Do NOT include /crewai-enterprise here — the platform appends it

  # ──────────────────────────────────────────
  # Authentication: Microsoft Entra ID
  # CLIENT_ID and TENANT_ID are non-sensitive identifiers — they belong under envVars
  # CLIENT_SECRET is a credential — it belongs under secrets (below)
  # ──────────────────────────────────────────
  AUTH_PROVIDER: "entra_id"                              # exact value — lowercase with underscore
  ENTRA_ID_CLIENT_ID: "<APPLICATION_CLIENT_ID>"          # from App Registration overview
  ENTRA_ID_TENANT_ID: "<DIRECTORY_TENANT_ID>"            # from App Registration overview

  # ──────────────────────────────────────────
  # Application
  # ──────────────────────────────────────────
  APPLICATION_HOST: "<YOUR_DOMAIN>"                      # e.g. crewai.company.com — no https:// prefix
  RAILS_LOG_LEVEL: "info"
  RAILS_MAX_THREADS: "5"
  DB_POOL: "5"

# ──────────────────────────────────────────────
# Secrets
# ──────────────────────────────────────────────
secrets:
  DB_PASSWORD: "<YOUR_DB_PASSWORD>"
  SECRET_KEY_BASE: "<64-char-hex-string>"                # openssl rand -hex 64
  ENTRA_ID_CLIENT_SECRET: "<CLIENT_SECRET_VALUE>"        # from Azure Certificates & Secrets

# ──────────────────────────────────────────────
# Wharf: OTLP trace and span collection
# wharf.enabled is the chart default (true) — included here for clarity
# Wharf shares the same DB host/port/user/password as the main app
# The wharf database must be pre-created in Azure PostgreSQL (see commands above)
# ──────────────────────────────────────────────
wharf:
  enabled: true
  postgres:
    wharfDatabase: "wharf"                               # must match the database you created above

# ──────────────────────────────────────────────
# Web: application pods
# ──────────────────────────────────────────────
web:
  replicaCount: 2
  enableSslFromPuma: false                               # REQUIRED when NGINX handles TLS termination
                                                         # Chart default is true — omitting causes NGINX to use
                                                         # the wrong backend protocol and return 502 errors
  resources:
    requests:
      cpu: "1000m"
      memory: "6Gi"
    limits:
      cpu: "6"
      memory: "12Gi"

  ingress:
    enabled: true
    className: "nginx"
    host: "<YOUR_DOMAIN>"                                # must match APPLICATION_HOST and TLS certificate

    annotations:
      cert-manager.io/cluster-issuer: "letsencrypt-prod"  # references the ClusterIssuer created above

    tls:
      - secretName: crewai-tls                          # cert-manager creates and manages this secret
        hosts:
          - "<YOUR_DOMAIN>"

# ──────────────────────────────────────────────
# Worker: background job processing
# ──────────────────────────────────────────────
worker:
  replicaCount: 2
  resources:
    requests:
      cpu: "1000m"
      memory: "6Gi"
    limits:
      cpu: "6"
      memory: "12Gi"

# ──────────────────────────────────────────────
# BuildKit: crew container image builds
# ──────────────────────────────────────────────
buildkit:
  enabled: true
  replicaCount: 1
  resources:
    requests:
      cpu: "500m"
      memory: "2Gi"
    limits:
      cpu: "4"
      memory: "8Gi"

# ──────────────────────────────────────────────
# RBAC: auto-creates ServiceAccount crewai-sa
# Required for Workload Identity — name must match the federated credential subject created above
# ──────────────────────────────────────────────
rbac:
  create: true

# ──────────────────────────────────────────────
# ServiceAccount: annotate with Managed Identity client ID
# Required for AKS Workload Identity to function
# ──────────────────────────────────────────────
serviceAccount:
  annotations:
    azure.workload.identity/client-id: "<MANAGED_IDENTITY_CLIENT_ID>"  # from az identity show --query clientId
web.enableSslFromPuma: false is required. The chart default is true. NGINX terminates TLS and forwards plain HTTP to backend pods. With the default true, Puma expects HTTPS connections but receives HTTP from NGINX, causing 502 errors on every request.
CREW_IMAGE_REGISTRY_OVERRIDE must not include /crewai-enterprise. The platform appends this suffix automatically. If your ACR image path is myregistry.azurecr.io/production/crewai-enterprise, set only myregistry.azurecr.io/production. Including the suffix causes push failures to a double-suffixed path.
POSTGRES_OAUTH_DB must be set explicitly. The chart default is oauth_db. If you do not override it to crewai_plus_oauth_production, the OAuth service will attempt to connect to a database that does not exist and fail on startup.
If you prefer to authenticate to Azure Blob Storage using a static storage account key instead of Workload Identity, remove the serviceAccount.annotations block and add AZURE_STORAGE_ACCESS_KEY: "<storage-account-key>" under secrets: instead. Workload Identity is strongly preferred for production deployments.

Install

helm install crewai-platform \
  oci://registry.crewai.com/crewai/stable/crewai-platform \
  --values values.yaml \
  --namespace crewai \
  --create-namespace
Wait for all pods to reach Running:
kubectl get pods -n crewai -w

Post-Install

Required Initialization

These commands must be completed before any user can log in. Run them in the order shown.
# 1. Initialize the internal CrewAI organization
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails studio:install_internal_organization

# 2. Set up default permission roles
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails factory:setup_permissions_defaults

# 3. Add your admin user as owner of org 2 (the first customer-facing org)
#    Replace admin@company.com with the email address assigned the factory-admin App Role in Azure
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails 'factory:add_owner[2,admin@company.com]'
Do NOT run factory:grant_admin for Entra ID deployments. Admin panel access is controlled by the factory-admin App Role in Azure portal. The factory:grant_admin command writes to a database table that Entra ID authentication does not consult — it has no effect on Entra ID users and will not grant admin access.
For Entra ID, the user record is created in the database automatically on first login. The factory:add_owner command above can be run before or after the user’s first login.

Studio V2 Setup

Studio V2 cannot be configured in values.yaml. Adding studioV2.enabled or STUDIO_V2_ENABLED has no effect — Helm silently ignores unknown keys. Setup requires the platform to be fully running and accessible. Step 1: Create the LLM Connection (UI)
  1. Log in to the CrewAI web UI as an admin
  2. Navigate to Settings → LLM Connections
  3. Click New Connection
  4. Set the name to exactly studio-v2 (lowercase, no spaces)
  5. Select your LLM provider, enter the model name and API key
  6. Click Save
The connection name must be exactly studio-v2. The install commands in Step 3 look up this name specifically — a different name or capitalization causes them to fail silently.
Step 2: Set as Default Connection (UI)
  1. Navigate to Settings → Crew Studio
  2. Under Default Connection, select studio-v2
  3. Click Save
Step 3: Run Install Commands (kubectl) Run these commands in order. Each must complete successfully before running the next.
# 1. Install the Studio agent
#    This command FAILS if the studio-v2 LLM Connection does not exist yet
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails studio:agent:install

# 2. Sync and index tools
kubectl exec -it deploy/crewai-web -n crewai -- \
  bin/rails studio:tools:sync_crewai_tools \
       studio:tools:sync_enterprise_tools

# 3. Install the Studio runner
kubectl exec -it deploy/crewai-web -n crewai -- bin/rails studio:runner:install
studio:agent:install will fail if the studio-v2 LLM Connection does not already exist in the UI. Complete Steps 1 and 2 before running any of these commands.

Verify

Platform Health

# All pods should be Running
kubectl get pods -n crewai

# Check web pod logs for errors
kubectl logs -l app.kubernetes.io/component=web --tail=50 -n crewai

# Check worker pod logs
kubectl logs -l app.kubernetes.io/component=worker --tail=50 -n crewai

NGINX Ingress

# ADDRESS should show an IP or hostname within 2–3 minutes
kubectl get ingress -n crewai
If ADDRESS is empty after 5 minutes, check the NGINX Ingress Controller logs:
kubectl logs -n ingress-nginx deployment/ingress-nginx-controller | tail -50
Check cert-manager certificate status:
kubectl get certificate -n crewai
kubectl describe certificate crewai-tls -n crewai
If the certificate is stuck in a non-Ready state, verify the ClusterIssuer is configured correctly and that the NGINX controller has a public IP (Let’s Encrypt requires HTTP-01 challenge access).

Authentication

  1. Navigate to https://<YOUR_DOMAIN>
  2. Click Sign in with Microsoft
  3. Authenticate with a user assigned a role in Azure portal
  4. Verify the user lands on the dashboard without error
If login fails with a redirect URI mismatch or authentication error, verify the redirect URI in Azure matches https://<YOUR_DOMAIN>/auth/entra_id/callback exactly.

Wharf Trace Collection

# Wharf pod should be Running
kubectl get pods -l app.kubernetes.io/component=wharf -n crewai

# Check Wharf logs
kubectl logs -l app.kubernetes.io/component=wharf --tail=30 -n crewai
Wharf connects to the wharf database on the same PostgreSQL Flexible Server as the main application. If the pod is in CrashLoopBackOff, verify the wharf database exists and the crewai user has access.

Studio V2

# Both deployments should show Running
kubectl get deploy -n crewai | grep -E "studio-assistant|studio-runner"

# Check Studio assistant logs
kubectl logs -l app=studio-assistant --tail=20 -n crewai

Factory Health Endpoint

TOKEN=$(kubectl get secret crewai-platform-secrets -n crewai \
  -o jsonpath='{.data.FACTORY_DEBUG_TOKEN}' | base64 -d)

curl -H "X-Factory-Debug-Token: $TOKEN" \
  https://<YOUR_DOMAIN>/health/debug
All components should report "status": "ok".