Azure Container Apps
Azure Container Apps is a managed container platform built on Kubernetes. It provides auto-scaling via KEDA, built-in ingress, and optional Dapr integration. CruzJS runs as a long-lived container with fire-and-forget waitUntil() support.
When to Use Container Apps
Section titled “When to Use Container Apps”- Sustained or high traffic
- You need
waitUntil()fire-and-forget behavior - You want managed identity (no passwords in config)
- You need custom system dependencies or binaries
- You want Dapr for service-to-service communication
For low traffic or event-driven workloads, consider Azure Functions instead.
Full Adapter Configuration
Section titled “Full Adapter Configuration”import { AzureContainerAppsAdapter } from '@cruzjs/adapter-azure';
export default createCruzApp({ schema, modules: [/* your modules */], adapter: new AzureContainerAppsAdapter({ databaseUrl: process.env.DATABASE_URL, redisUrl: process.env.REDIS_URL, azureStorageConnectionString: process.env.AZURE_STORAGE_CONNECTION_STRING, azureStorageContainer: process.env.AZURE_STORAGE_CONTAINER, serviceBusConnectionString: process.env.SERVICE_BUS_CONNECTION_STRING, serviceBusQueueName: process.env.SERVICE_BUS_QUEUE_NAME, }), pages: () => import('virtual:react-router/server-build'),});waitUntil() Behavior
Section titled “waitUntil() Behavior”Container Apps runs as container runtime type. The container stays alive between requests, so waitUntil() tasks execute as fire-and-forget — the response is sent immediately and background work continues.
Auto-Scaling with KEDA
Section titled “Auto-Scaling with KEDA”Container Apps uses KEDA for event-driven scaling. The default HTTP scaler works for most CruzJS apps:
az containerapp create \ --resource-group my-cruz-rg \ --name my-cruz-app \ --environment my-cruz-env \ --image mycruzregistry.azurecr.io/my-cruz-app:latest \ --target-port 3000 \ --ingress external \ --min-replicas 1 \ --max-replicas 10 \ --cpu 0.5 \ --memory 1Gi \ --scale-rule-name http-rule \ --scale-rule-http-concurrency 50| Setting | Purpose |
|---|---|
--min-replicas 1 | Avoids cold starts by keeping one instance warm |
--min-replicas 0 | Enables scale-to-zero for dev/staging |
--scale-rule-http-concurrency 50 | Scale up when requests per instance exceed 50 |
Managed Identity
Section titled “Managed Identity”Managed identity is the recommended way to connect to Azure services in production. It eliminates passwords and connection strings.
# Enable system-assigned identityaz containerapp identity assign \ --resource-group my-cruz-rg \ --name my-cruz-app \ --system-assigned
# Grant access to PostgreSQLaz postgres flexible-server ad-admin create \ --resource-group my-cruz-rg \ --server-name my-cruz-db \ --display-name my-cruz-app \ --object-id $(az containerapp identity show --resource-group my-cruz-rg --name my-cruz-app --query principalId -o tsv)
# Grant access to Blob Storageaz role assignment create \ --assignee $(az containerapp identity show --resource-group my-cruz-rg --name my-cruz-app --query principalId -o tsv) \ --role "Storage Blob Data Contributor" \ --scope /subscriptions/.../resourceGroups/my-cruz-rg/providers/Microsoft.Storage/storageAccounts/mycruzuploadsDockerfile
Section titled “Dockerfile”FROM node:20-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npx cruz build
FROM node:20-alpineWORKDIR /appCOPY --from=builder /app/build ./buildCOPY --from=builder /app/node_modules ./node_modulesCOPY --from=builder /app/package.json ./ENV PORT=3000EXPOSE 3000CMD ["node", "build/server/index.js"]Health Probes
Section titled “Health Probes”Container Apps supports liveness, readiness, and startup probes:
az containerapp update \ --resource-group my-cruz-rg \ --name my-cruz-app \ --set-env-vars="HEALTH_CHECK_PATH=/healthz" \ --probe-type liveness \ --probe-path /healthz \ --probe-period 10 \ --probe-timeout 5 \ --probe-failure-threshold 3CruzJS includes a built-in /healthz endpoint that returns 200 when the app is ready.
Dapr Integration (Optional)
Section titled “Dapr Integration (Optional)”Dapr provides service invocation, pub/sub, and state management as sidecar capabilities:
az containerapp dapr enable \ --resource-group my-cruz-rg \ --name my-cruz-app \ --dapr-app-id cruz-app \ --dapr-app-port 3000 \ --dapr-app-protocol httpDapr is optional. CruzJS works without it using direct service connections.
Deployment
Section titled “Deployment”# Create a Container Apps environmentaz containerapp env create \ --resource-group my-cruz-rg \ --name my-cruz-env \ --location eastus
# Build and push via Azure Container Registryaz acr build \ --registry mycruzregistry \ --image my-cruz-app:latest .
# Deployaz containerapp create \ --resource-group my-cruz-rg \ --name my-cruz-app \ --environment my-cruz-env \ --image mycruzregistry.azurecr.io/my-cruz-app:latest \ --registry-server mycruzregistry.azurecr.io \ --target-port 3000 \ --ingress external \ --env-vars \ DATABASE_URL="postgresql://cruzuser:pw@my-cruz-db.postgres.database.azure.com:5432/cruzdb?sslmode=require" \ AUTH_SECRET=secretref:auth-secret \ REDIS_URL="rediss://:key@my-cruz-cache.redis.cache.windows.net:6380" \ --min-replicas 1 \ --max-replicas 10 \ --cpu 0.5 \ --memory 1GiNext Steps
Section titled “Next Steps”- Deploying to Azure — CI/CD, Bicep, and migration workflows
- Azure Functions — serverless alternative
- Azure Adapter Overview — service mapping and environment variables