Skip to main content
Version: 1.1.0

Cloud Provider Deployment

Deploy DVARA on AWS (EKS or ECS), Google Cloud, Azure, or DigitalOcean with provider-specific configurations and best practices.

Most sections below target managed Kubernetes and build on the Kubernetes / Helm chart — read that first. The AWS (ECS) section is the exception: it covers a Fargate-based deployment that skips Kubernetes entirely.


AWS (EKS)

Prerequisites

  • eksctl or an existing EKS cluster (Kubernetes 1.28+)
  • AWS CLI configured with appropriate permissions
  • Helm 3.x installed

Create an EKS Cluster

eksctl create cluster \
--name dvara \
--region us-east-1 \
--nodegroup-name standard \
--node-type m6i.large \
--nodes 3 \
--nodes-min 2 \
--nodes-max 5 \
--managed

Install the AWS Load Balancer Controller

Required for ALB-based Ingress:

# Install the controller (see AWS docs for full IAM setup)
helm repo add eks https://aws.github.io/eks-charts
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=dvara \
--set serviceAccount.create=true

Store Secrets in AWS Secrets Manager

# Create the secret
aws secretsmanager create-secret \
--name dvara/provider-credentials \
--secret-string '{
"openai-api-key": "sk-...",
"anthropic-api-key": "sk-ant-...",
"enterprise-license-key": "DVARA-..."
}'

Use the External Secrets Operator to sync into Kubernetes:

# external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: dvara-provider-keys
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: dvara-external
data:
- secretKey: openai-api-key
remoteRef:
key: dvara/provider-credentials
property: openai-api-key
- secretKey: anthropic-api-key
remoteRef:
key: dvara/provider-credentials
property: anthropic-api-key
- secretKey: enterprise-license-key
remoteRef:
key: dvara/provider-credentials
property: enterprise-license-key
kubectl apply -f external-secret.yaml

Deploy with ALB Ingress + IRSA

# aws-values.yaml
gatewayServer:
replicaCount: 3
javaOpts: "-Xms512m -Xmx768m"

serviceAccount:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/dvara-gateway

autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70

pdb:
enabled: true
minAvailable: 2

serviceMonitor:
enabled: true
additionalLabels:
release: kube-prometheus-stack

topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/component: gateway-server

gatewayUi:
enabled: true

secrets:
create: false
existingSecret: dvara-external

ingress:
enabled: true
className: alb
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:123456789012:certificate/abc-123
alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06
alb.ingress.kubernetes.io/healthcheck-path: /actuator/health/readiness
gatewayServer:
hosts:
- host: gateway.mycompany.com
paths:
- path: /
pathType: Prefix
gatewayUi:
hosts:
- host: admin.mycompany.com
paths:
- path: /
pathType: Prefix
helm install dvara charts/dvara/ --version 1.0.0 -f aws-values.yaml

AWS Bedrock with IRSA (No API Keys)

Use IAM Roles for Service Accounts to access Bedrock without static credentials:

# Create the IAM policy
cat > bedrock-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": "arn:aws:bedrock:*:*:model/*"
}]
}
EOF

aws iam create-policy \
--policy-name DVARABedrockAccess \
--policy-document file://bedrock-policy.json

# Associate with the service account
eksctl create iamserviceaccount \
--name dvara-server \
--namespace default \
--cluster dvara \
--attach-policy-arn arn:aws:iam::123456789012:policy/DVARABedrockAccess \
--approve
# bedrock-values.yaml
gatewayServer:
serviceAccount:
create: false
name: dvara-server

secrets:
bedrockEnabled: "true"
helm install dvara charts/dvara/ --version 1.0.0 -f bedrock-values.yaml

Database for Enterprise Persistence

PostgreSQL is required for all DVARA deployments. Rate limiting and API-key caching run in-process — no external cache infrastructure is required.

RDS PostgreSQL:

aws rds create-db-instance \
--db-instance-identifier dvara-db \
--db-instance-class db.r6g.large \
--engine postgres --engine-version 14 \
--master-username dvara --master-user-password "${DB_PASSWORD}" \
--allocated-storage 100 --storage-encrypted \
--vpc-security-group-ids sg-... --db-subnet-group-name dvara-subnet

Helm values for database:

gatewayServer:
env:
- name: SPRING_DATASOURCE_URL
value: "jdbc:postgresql://dvara-db.xxx.us-east-1.rds.amazonaws.com:5432/dvara"
- name: SPRING_DATASOURCE_USERNAME
value: "dvara"
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: dvara-db-credentials
key: password

Multi-Region on AWS

Primary region (us-east-1):

# primary-values.yaml
gatewayServer:
replicaCount: 3
region:
id: us-east-1
name: US East
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10

gatewayUi:
enabled: true

secrets:
providerKeys:
openai: sk-...
gatewayInternalSecret: "shared-internal-secret-change-me"
enterpriseLicenseKey: "DVARA-..."

Secondary region (eu-west-1) — syncs config from the primary:

# secondary-eu-values.yaml
gatewayServer:
replicaCount: 2
region:
id: eu-west-1
name: EU West
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
env: []
# Config propagation uses PostgreSQL NOTIFY/LISTEN — all regions
# share the same PostgreSQL instance (or read replica) and receive
# config changes automatically. No additional sync env vars needed.

gatewayUi:
enabled: false

secrets:
providerKeys:
openai: sk-...
enterpriseLicenseKey: "DVARA-..."
# Deploy primary in us-east-1
kubectl config use-context us-east-1
helm install dvara charts/dvara/ --version 1.0.0 -f primary-values.yaml

# Deploy secondary in eu-west-1
kubectl config use-context eu-west-1
helm install dvara-eu charts/dvara/ --version 1.0.0 -f secondary-eu-values.yaml

AWS (ECS)

AWS ECS on Fargate is a lower-operational-overhead alternative to EKS. If you don't want to run a Kubernetes control plane, ECS runs the same DVARA container images (ghcr.io/dvarahq/dvara/*) as serverless tasks behind an Application Load Balancer.

DVARA's data plane and DVARA Flightdeck are both stateless — the only persistent state lives in RDS PostgreSQL, so ECS task restarts and deployments are safe.

Prerequisites

  • AWS CLI configured with an account that can create ECS clusters, task definitions, Secrets Manager secrets, and ALBs
  • An existing VPC with at least two private subnets (for Fargate tasks) and two public subnets (for the ALB)
  • A running RDS PostgreSQL instance reachable from the VPC's private subnets (see Database Requirements)
  • A valid DVARA license key and at least one provider API key

Store secrets in AWS Secrets Manager

DVARA has first-class AWS Secrets Manager support, so you can either reference secrets directly at task-definition time (recommended — injected as env vars by ECS) or let DVARA fetch them at runtime via the vault integration.

aws secretsmanager create-secret \
--name dvara/license \
--secret-string "DVARA-...your-license-key"

aws secretsmanager create-secret \
--name dvara/openai \
--secret-string "sk-..."

aws secretsmanager create-secret \
--name dvara/db \
--secret-string '{"username":"dvara","password":"..."}'

Task execution role

The ECS task execution role needs permission to pull from GHCR (via a secret holding a GHCR personal access token), read the secrets you just created, and write to CloudWatch Logs:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"kms:Decrypt"
],
"Resource": [
"arn:aws:secretsmanager:us-east-1:<account>:secret:dvara/*"
]
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}

Task definition

Register one task definition with both gateway-server and gateway-ui as containers in the same task (simple), or split them across two task definitions to scale independently (recommended for production). This example shows the split:

{
"family": "dvara-gateway",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "2048",
"executionRoleArn": "arn:aws:iam::<account>:role/dvara-ecs-task-exec",
"taskRoleArn": "arn:aws:iam::<account>:role/dvara-ecs-task",
"containerDefinitions": [
{
"name": "gateway-server",
"image": "ghcr.io/dvarahq/dvara/dvara-llm-gateway:1.0.0",
"essential": true,
"portMappings": [
{ "containerPort": 8080, "protocol": "tcp" }
],
"secrets": [
{ "name": "DVARA_LICENSE_KEY", "valueFrom": "arn:aws:secretsmanager:us-east-1:<account>:secret:dvara/license" },
{ "name": "OPENAI_API_KEY", "valueFrom": "arn:aws:secretsmanager:us-east-1:<account>:secret:dvara/openai" },
{ "name": "SPRING_DATASOURCE_USERNAME","valueFrom": "arn:aws:secretsmanager:us-east-1:<account>:secret:dvara/db:username::" },
{ "name": "SPRING_DATASOURCE_PASSWORD","valueFrom": "arn:aws:secretsmanager:us-east-1:<account>:secret:dvara/db:password::" }
],
"environment": [
{ "name": "SPRING_DATASOURCE_URL", "value": "jdbc:postgresql://dvara.cluster-abc.us-east-1.rds.amazonaws.com:5432/dvara" }
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/dvara/gateway-server",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
},
"healthCheck": {
"command": ["CMD", "curl", "-sf", "http://localhost:8080/actuator/health"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
}
}
]
}

Register it and create the service:

aws ecs register-task-definition --cli-input-json file://dvara-gateway-taskdef.json

aws ecs create-service \
--cluster dvara \
--service-name dvara-gateway \
--task-definition dvara-gateway \
--desired-count 3 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-aaa,subnet-bbb],securityGroups=[sg-dvara],assignPublicIp=DISABLED}" \
--load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/dvara-gateway/xxx,containerName=gateway-server,containerPort=8080"

Create a similar task definition and service for dvara-flightdeck (port 8090, image ghcr.io/dvarahq/dvara/dvara-flightdeck:1.0.0, with DVARA_FLIGHTDECK_GATEWAY_URL pointing at the dvara-gateway ALB target group or the ALB's internal URL).

Application Load Balancer

Create an ALB with two target groups — one for gateway-server (port 8080) and one for gateway-ui (port 8090). Route /v1/* to the gateway target group and everything else to the DVARA Flightdeck target group, or use two separate ALBs / listener rules.

Health check paths on the ALB target groups:

  • dvara-gateway target group: GET /actuator/health (port 8080) — anonymous, no Bearer needed
  • dvara-admin target group: GET /actuator/health/readiness (port 8090)

Clustering on Fargate

DVARA LLM Gateway instances share rate-limit counters and API key lookups across the fleet. On Fargate, the tasks need to discover each other. Fargate doesn't support multicast, so configure TCP-IP discovery via AWS ECS service discovery (Cloud Map):

  1. Create a Cloud Map namespace (e.g., dvara.local) attached to your VPC.
  2. Register the dvara-gateway service with Cloud Map — this gives every task an A record at gateway-server.dvara.local.
  3. Point the cluster's TCP-IP discovery member list at gateway-server.dvara.local so new tasks find their siblings via DNS resolution. The exact env-var name is documented in Configuration → Rate Limiting; set it on every gateway-server task.

See Configuration → Rate Limiting for the full property list.

Auto scaling

Use ECS service auto scaling with target tracking on ECSServiceAverageCPUUtilization or a custom CloudWatch metric (e.g., gateway_requests_total rate scraped from Prometheus via the AWS Distro for OpenTelemetry collector).

aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/dvara/dvara-gateway \
--min-capacity 3 \
--max-capacity 20

aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/dvara/dvara-gateway \
--policy-name cpu-tracking \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration file://cpu-tracking.json

Observability

  • Logs — DVARA emits structured JSON by default. Point the awslogs log driver at CloudWatch Logs, then ship to your SIEM via subscription filters (Kinesis Firehose → S3 / OpenSearch / Splunk).
  • Metrics — use AWS Distro for OpenTelemetry (ADOT) as a sidecar container to scrape /actuator/prometheus on each task and forward to Amazon Managed Prometheus or CloudWatch. The scrape requires Authorization: Bearer $DVARA_ACTUATOR_METRICS_API_KEY — mount the metrics key as a file (for example via Secrets Manager) and configure the ADOT receiver's bearer_token_file. The metrics secret is intentionally distinct from DVARA_ACTUATOR_API_KEY so a leaked scrape token can't unlock the rich gateway status surface.
  • Tracing — set OTEL_EXPORTER_OTLP_ENDPOINT to the ADOT sidecar (http://localhost:4318/v1/traces) to ship traces to AWS X-Ray.

When to choose ECS over EKS

ECS (Fargate)EKS
Cluster opsNone (serverless)Control plane + node groups
Helm / kubectlNoYes
Right forSmall-to-medium deployments, teams without Kubernetes expertiseLarge fleets, teams already running Kubernetes
DVARA featuresAllAll
Cost modelPay per taskPay per node + control plane

ECS is the simpler path if you're new to AWS or don't already have Kubernetes in production. EKS wins once you need advanced networking (Istio, Linkerd), custom autoscaling, or already have a fleet of charts to reuse.


Google Cloud (GKE)

Run DVARA on GKE with Cloud SQL (managed PostgreSQL, private IP — a direct connection that keeps config hot-reload working), Workload Identity for secrets, and a GCE Ingress fronted by a Google-managed TLS certificate. This builds on the Kubernetes / Helm chart — read that first.

Scripted recipe

A complete, runnable version of everything below — gcloud provisioning + helm install + verification in one script — lives in kubernetes/gke/ in dvara-examples. This page is the explained walkthrough; the recipe is the copy-paste path.

Prerequisites

  • gcloud CLI authenticated against a billing-enabled project
  • kubectl and Helm 3.8+
  • A DVARA license envelope (DVARA-…). Every DVARA process refuses to boot without one; there is no operator bypass.

Create a VPC-native GKE cluster

Workload Identity (--workload-pool) and VPC-native networking (--enable-ip-alias) are both required — the first lets pods authenticate to Google APIs without static keys; the second lets the cluster reach Cloud SQL over a private IP.

PROJECT_ID=my-project
gcloud container clusters create dvara \
--region us-central1 \
--num-nodes 1 \
--release-channel regular \
--enable-ip-alias \
--workload-pool="${PROJECT_ID}.svc.id.goog"

gcloud container clusters get-credentials dvara --region us-central1
kubectl create namespace dvara

Cloud SQL (private IP)

DVARA propagates config changes (routes, policies) from Flightdeck to the data plane over PostgreSQL LISTEN/NOTIFY. That requires a direct, session-level connection — so use Cloud SQL's private IP, not a transaction-mode pooler in front of it. (A transaction pooler silently breaks NOTIFY and config hot-reload stops working.)

# One-time: private services access so Cloud SQL gets a private IP
gcloud compute addresses create google-managed-services-default \
--global --purpose=VPC_PEERING --prefix-length=16 --network=default
gcloud services vpc-peerings connect \
--service=servicenetworking.googleapis.com \
--ranges=google-managed-services-default --network=default

# Private-IP-only Postgres 16 instance
# --edition=ENTERPRISE is required: new Cloud SQL instances default to
# ENTERPRISE_PLUS, which rejects db-custom-* tiers (it wants db-perf-optimized-*).
gcloud sql instances create dvara-pg \
--database-version=POSTGRES_16 --tier=db-custom-1-3840 --edition=ENTERPRISE \
--region=us-central1 \
--network="projects/${PROJECT_ID}/global/networks/default" \
--no-assign-ip
gcloud sql databases create dvara --instance=dvara-pg
gcloud sql users create dvara --instance=dvara-pg --password="${DB_PASSWORD}"

# Capture the private IP for the JDBC URL below
gcloud sql instances describe dvara-pg \
--format='value(ipAddresses.filter("type=PRIVATE").extract("ipAddress").flatten())'
Why private IP, not the Cloud SQL Auth Proxy

The Auth Proxy normally runs as a per-pod sidecar, and the DVARA chart has no extraContainers knob to inject one. Private IP needs no sidecar — the gateway connects with a plain JDBC URL. (Once the poll-based config refresh ships in a later release, the connection becomes pooler-agnostic and this constraint relaxes.)

Workload Identity for secrets

Make Google Secret Manager the source of truth and sync into Kubernetes with the External Secrets Operator, authenticated by the pods' Workload-Identity-bound service account — no secret ever rides a helm --set command line.

DVARA needs five operational secrets (each openssl rand -base64 32 except the license):

Secret (k8s key)Purpose
enterprise-license-keySigned DVARA-… envelope; validated at boot
gateway-encryption-master-passwordAES-256-GCM key for ENCRYPTED-mode provider credentials — loss is unrecoverable, escrow offline
gateway-server-api-keyBearer for /actuator/gateway-status
gateway-metrics-api-keyBearer for /actuator/prometheusmust differ from the above
audit-hmac-secretHMAC-SHA256 key signing the audit chain
# Push to Secret Manager
for s in license-key encryption-master-password actuator-api-key \
actuator-metrics-api-key audit-hmac-secret; do
gcloud secrets create "dvara-${s}" --replication-policy=automatic
done
# (add a version to each with `gcloud secrets versions add … --data-file=-`)

# GCP service account + Workload Identity binding to the chart's two KSAs
gcloud iam service-accounts create dvara-gke
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:dvara-gke@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
for ksa in dvara-gateway-server dvara-flightdeck; do
gcloud iam service-accounts add-iam-policy-binding \
"dvara-gke@${PROJECT_ID}.iam.gserviceaccount.com" \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:${PROJECT_ID}.svc.id.goog[dvara/${ksa}]"
done
# external-secret.yaml — syncs Secret Manager → the k8s Secret the chart consumes
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: dvara-platform
namespace: dvara
spec:
refreshInterval: 1h
secretStoreRef: { name: gcp-secret-manager, kind: ClusterSecretStore }
target: { name: dvara-platform }
data:
- { secretKey: enterprise-license-key, remoteRef: { key: dvara-license-key } }
- { secretKey: gateway-encryption-master-password, remoteRef: { key: dvara-encryption-master-password } }
- { secretKey: gateway-server-api-key, remoteRef: { key: dvara-actuator-api-key } }
- { secretKey: gateway-metrics-api-key, remoteRef: { key: dvara-actuator-metrics-api-key } }
- { secretKey: audit-hmac-secret, remoteRef: { key: dvara-audit-hmac-secret } }
Provider keys aren't here

DVARA uses the BYOK model — tenant provider keys live AES-encrypted in the database and are added via the Console after install, not as env vars. The chart's provider-key references are all optional: true, so the synced Secret only needs the five platform keys above.

Deploy with GCE Ingress + managed TLS

# gke-values.yaml
gatewayServer:
replicaCount: 2
javaOpts: "-Xms256m -Xmx768m"
serviceAccount:
create: true
name: dvara-gateway-server # pinned → matches the WI binding above
annotations:
iam.gke.io/gcp-service-account: dvara-gke@my-project.iam.gserviceaccount.com
extraEnv:
- { name: SPRING_DATASOURCE_URL, value: "jdbc:postgresql://CLOUDSQL_PRIVATE_IP:5432/dvara?sslmode=require" }
- { name: SPRING_DATASOURCE_USERNAME, value: "dvara" }
- name: SPRING_DATASOURCE_PASSWORD
valueFrom: { secretKeyRef: { name: dvara-db, key: password } }
pdb: { enabled: true, minAvailable: 1 }

flightdeck:
enabled: true
serviceAccount:
create: true
name: dvara-flightdeck
annotations:
iam.gke.io/gcp-service-account: dvara-gke@my-project.iam.gserviceaccount.com
extraEnv:
- { name: SPRING_DATASOURCE_URL, value: "jdbc:postgresql://CLOUDSQL_PRIVATE_IP:5432/dvara?sslmode=require" }
- { name: SPRING_DATASOURCE_USERNAME, value: "dvara" }
- name: SPRING_DATASOURCE_PASSWORD
valueFrom: { secretKeyRef: { name: dvara-db, key: password } }

secrets:
create: false
existingSecret: dvara-platform # synced by External Secrets above

# GCE Ingress: explicit hosts + empty tls so NO spec.tls renders — Google
# managed certs are driven by the annotation + the ManagedCertificate CRD.
ingress:
enabled: true
className: gce
annotations:
kubernetes.io/ingress.global-static-ip-name: "dvara-ip"
networking.gke.io/managed-certificates: "dvara-cert"
kubernetes.io/ingress.allow-http: "false"
gatewayServer:
hosts: [{ host: api.mycompany.com, paths: [{ path: /, pathType: Prefix }] }]
tls: []
flightdeck:
hosts: [{ host: flightdeck.mycompany.com, paths: [{ path: /, pathType: Prefix }] }]
tls: []
# managed-certificate.yaml
apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata: { name: dvara-cert, namespace: dvara }
spec:
domains: [api.mycompany.com, flightdeck.mycompany.com]
# Reserve the static IP the Ingress annotation references
gcloud compute addresses create dvara-ip --global

kubectl -n dvara create secret generic dvara-db --from-literal=password="${DB_PASSWORD}"
kubectl -n dvara apply -f external-secret.yaml
kubectl -n dvara apply -f managed-certificate.yaml

helm install dvara oci://ghcr.io/dvarahq/charts/dvara \
--version 1.0.1 \
--namespace dvara \
--values gke-values.yaml \
--set gatewayServer.image.tag=1.0.1 \
--set flightdeck.image.tag=1.0.1 \
--wait
Pin the image tag — never :latest

The chart version and image tags above are pinned to 1.0.1, the current published release on GHCR. Pin a real, published tag so the deploy is reproducible and rollbacks are clean — :latest is a moving pointer and can change under you. When 1.1.0 is released and its images are published, bump these to 1.1.0. Don't pin a tag that hasn't shipped yet, or pods fail with ImagePullBackOff.

After DNS A-records for both hosts point at the static IP, the Google-managed certificate provisions in 15–60 minutes (kubectl -n dvara describe managedcertificate dvara-cert → status Active). The Ingress serves HTTP 503 until then.

Verify (including config hot-reload)

kubectl -n dvara get pods # all Ready
kubectl -n dvara port-forward svc/dvara-flightdeck 8090:8090 &
open http://localhost:8090/ # walk /setup → tenant + API key

The chart-specific acceptance check is config hot-reload: create or edit a route in the Console, fire a request through the gateway, and confirm the new routing takes effect without restarting any pod. That proves the data plane picked up the change from PostgreSQL over NOTIFY/LISTEN — which works here precisely because Cloud SQL is reached on a direct private-IP connection.

Gemini / Vertex AI (optional)

The same Workload-Identity service account can grant Vertex AI access without an API key:

gcloud projects add-iam-policy-binding my-project \
--member="serviceAccount:dvara-gke@my-project.iam.gserviceaccount.com" \
--role="roles/aiplatform.user"

Or pass a Gemini API key as a BYOK credential in the Console.


Azure (AKS)

Prerequisites

  • Azure CLI configured
  • An existing AKS cluster or permissions to create one
  • Helm 3.x installed

Create an AKS Cluster

az group create --name dvara-rg --location eastus

az aks create \
--resource-group dvara-rg \
--name dvara \
--node-count 3 \
--node-vm-size Standard_D4s_v3 \
--enable-cluster-autoscaler \
--min-count 2 \
--max-count 6 \
--enable-managed-identity \
--enable-workload-identity \
--enable-oidc-issuer

az aks get-credentials --resource-group dvara-rg --name dvara

Store Secrets in Azure Key Vault

# Create key vault
az keyvault create --name dvara-vault --resource-group dvara-rg --location eastus

# Store secrets
az keyvault secret set --vault-name dvara-vault --name openai-api-key --value "sk-..."
az keyvault secret set --vault-name dvara-vault --name anthropic-api-key --value "sk-ant-..."
az keyvault secret set --vault-name dvara-vault --name enterprise-license-key --value "DVARA-..."

Use the Azure Key Vault Provider for Secrets Store CSI Driver:

# secret-provider-class.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: dvara-keyvault
spec:
provider: azure
parameters:
usePodIdentity: "false"
useVMManagedIdentity: "true"
userAssignedIdentityID: "<managed-identity-client-id>"
keyvaultName: dvara-vault
tenantId: "<azure-tenant-id>"
objects: |
array:
- |
objectName: openai-api-key
objectType: secret
- |
objectName: anthropic-api-key
objectType: secret
- |
objectName: enterprise-license-key
objectType: secret
secretObjects:
- secretName: dvara-external
type: Opaque
data:
- objectName: openai-api-key
key: openai-api-key
- objectName: anthropic-api-key
key: anthropic-api-key
- objectName: enterprise-license-key
key: enterprise-license-key
kubectl apply -f secret-provider-class.yaml

Deploy with NGINX Ingress Controller

# Install NGINX Ingress Controller
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz
# azure-values.yaml
gatewayServer:
replicaCount: 3
javaOpts: "-Xms512m -Xmx768m"

autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10

pdb:
enabled: true
minAvailable: 2

serviceMonitor:
enabled: true

topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/component: gateway-server

gatewayUi:
enabled: true

secrets:
create: false
existingSecret: dvara-external

ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
gatewayServer:
hosts:
- host: gateway.mycompany.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: gateway-tls
hosts:
- gateway.mycompany.com
gatewayUi:
hosts:
- host: admin.mycompany.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: admin-tls
hosts:
- admin.mycompany.com
helm install dvara charts/dvara/ --version 1.0.0 -f azure-values.yaml

Database for Enterprise Persistence

PostgreSQL is required for all DVARA deployments. Rate limiting and API-key caching run in-process — no external cache infrastructure is required.

Azure Database for PostgreSQL:

az postgres flexible-server create \
--resource-group dvara-rg --name dvara-db \
--version 14 --sku-name Standard_D2s_v3 --storage-size 128 \
--admin-user dvara --admin-password "${DB_PASSWORD}" \
--tier GeneralPurpose --high-availability ZoneRedundant

Helm values: use the same SPRING_DATASOURCE_* environment variables shown in the AWS section, substituting the Azure Database for PostgreSQL endpoint.

Azure OpenAI Service

To use Azure OpenAI instead of the public OpenAI API, configure the Azure OpenAI provider:

# azure-openai-values.yaml
gatewayServer:
env:
- name: AZURE_OPENAI_API_KEY
value: "<azure-openai-api-key>"
- name: AZURE_OPENAI_BASE_URL
value: "https://my-resource.openai.azure.com/openai"

DigitalOcean (DOKS — Managed Kubernetes)

Run DVARA on DigitalOcean Kubernetes (DOKS) with DO Managed PostgreSQL (direct connection — keeps config hot-reload working) and Traefik v3 ingress with built-in Let's Encrypt. This builds on the Kubernetes / Helm chart. For the no-Kubernetes path, see DigitalOcean (App Platform) below.

Scripted recipe

A runnable version — doctl provisioning + Traefik + helm install + verification in one script — lives in kubernetes/doks/ in dvara-examples. This page is the explained walkthrough.

Prerequisites

  • doctl authenticated (doctl auth init), plus kubectl and Helm 3.8+
  • A DVARA license envelope (DVARA-…) and a domain for Let's Encrypt

Create a DOKS cluster + Managed PostgreSQL

doctl kubernetes cluster create dvara \
--region nyc1 \
--node-pool "name=default;size=s-2vcpu-4gb;count=2" --wait
doctl kubernetes cluster kubeconfig save dvara
CLUSTER_ID=$(doctl kubernetes cluster get dvara --format ID --no-header)

doctl databases create dvara-pg --engine pg --version 16 \
--region nyc1 --size db-s-1vcpu-2gb --num-nodes 1 --wait
DB_ID=$(doctl databases list --format ID,Name --no-header | awk '$2=="dvara-pg"{print $1}')

# Lock the DB to the cluster, then create the app database + user
doctl databases firewalls append "$DB_ID" --rule "k8s:${CLUSTER_ID}"
doctl databases db create "$DB_ID" dvara
doctl databases user create "$DB_ID" dvara

# Private connection details (port 25060, sslmode=require)
doctl databases connection "$DB_ID" --private --format Host --no-header
doctl databases user get "$DB_ID" dvara --format Password --no-header
Use the direct connection, not a transaction-mode pool

DVARA propagates config (routes, policies) from Flightdeck to the data plane over PostgreSQL LISTEN/NOTIFY, which needs a direct or session-level connection. The DSN above uses the direct host on port 25060. If you front the database with a DO Connection Pool, set it to session mode — a transaction-mode pool silently breaks NOTIFY and config hot-reload stops working. This is the failure mode most specific to managed-DO.

Secrets

DigitalOcean has no Workload-Identity / Secret-Manager equivalent, so the five platform secrets are a plain Kubernetes Secret (or point secrets.existingSecret at one synced by your own Vault / Doppler). Each is openssl rand -base64 32 except the license; the two actuator Bearers must differ.

kubectl create namespace dvara
kubectl -n dvara create secret generic dvara-db \
--from-literal=password="$DB_PASSWORD"
kubectl -n dvara create secret generic dvara-platform \
--from-literal=enterprise-license-key="$DVARA_LICENSE_KEY" \
--from-literal=gateway-encryption-master-password="$(openssl rand -base64 32)" \
--from-literal=gateway-server-api-key="$(openssl rand -base64 32)" \
--from-literal=gateway-metrics-api-key="$(openssl rand -base64 32)" \
--from-literal=audit-hmac-secret="$(openssl rand -base64 32)"

Provider keys aren't here — DVARA uses BYOK (tenant keys live AES-encrypted in the database, added via the Console after install). The chart's provider-key references are optional: true, so this Secret only needs the five platform keys.

Ingress — Traefik v3 (not ingress-nginx)

ingress-nginx reached end of life in March 2026, so this path uses Traefik v3, which has a built-in ACME client (no separate cert-manager):

helm repo add traefik https://traefik.github.io/charts && helm repo update
helm upgrade --install traefik traefik/traefik \
--namespace traefik --create-namespace \
--set "certificatesResolvers.letsencrypt.acme.email=ops@example.com" \
--set "certificatesResolvers.letsencrypt.acme.storage=/data/acme.json" \
--set "certificatesResolvers.letsencrypt.acme.tlsChallenge=true" \
--set "persistence.enabled=true" --wait

(Traefik Helm value paths are version-sensitive — confirm with helm show values traefik/traefik.)

Deploy

# doks-values.yaml
gatewayServer:
replicaCount: 2
serviceAccount: { create: true } # no cloud identity needed on DO
extraEnv:
- { name: SPRING_DATASOURCE_URL, value: "jdbc:postgresql://DB_PRIVATE_HOST:25060/dvara?sslmode=require" }
- { name: SPRING_DATASOURCE_USERNAME, value: "dvara" }
- name: SPRING_DATASOURCE_PASSWORD
valueFrom: { secretKeyRef: { name: dvara-db, key: password } }
pdb: { enabled: true, minAvailable: 1 }

flightdeck:
enabled: true
extraEnv:
- { name: SPRING_DATASOURCE_URL, value: "jdbc:postgresql://DB_PRIVATE_HOST:25060/dvara?sslmode=require" }
- { name: SPRING_DATASOURCE_USERNAME, value: "dvara" }
- name: SPRING_DATASOURCE_PASSWORD
valueFrom: { secretKeyRef: { name: dvara-db, key: password } }

secrets:
create: false
existingSecret: dvara-platform

# Traefik issues Let's Encrypt certs for the baseDomain-derived hosts
# (api.<domain> + flightdeck.<domain>).
ingress:
enabled: true
className: traefik
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
baseDomain: dvara.example.com
helm install dvara oci://ghcr.io/dvarahq/charts/dvara \
--version 1.0.1 --namespace dvara \
--values doks-values.yaml \
--set gatewayServer.image.tag=1.0.1 \
--set flightdeck.image.tag=1.0.1 --wait
Pin the image tag — never :latest

The chart version and image tags are pinned to 1.0.1, the current published release on GHCR. Pin a real, published tag so deploys are reproducible and rollbacks clean. When 1.1.0 is released, bump to 1.1.0 — don't pin a tag that hasn't shipped, or pods fail with ImagePullBackOff.

Point api.dvara.example.com and flightdeck.dvara.example.com at the DO Load Balancer IP (kubectl -n traefik get svc traefik); Traefik issues the certs within a few minutes of DNS resolving.

Verify (including config hot-reload)

kubectl -n dvara get pods # all Ready
kubectl -n dvara port-forward svc/dvara-flightdeck 8090:8090 &
open http://localhost:8090/ # /setup → tenant + API key

The chart-specific acceptance check is config hot-reload: create or edit a route in the Console, fire a request through the gateway, and confirm the new routing takes effect without restarting any pod — proof the data plane picked up the change over NOTIFY/LISTEN on the direct :25060 connection.


DigitalOcean (App Platform)

Prerequisites

  • doctl CLI configured
  • A DigitalOcean account

One-Click Deploy

Click the deploy button or use the CLI:

doctl apps create --spec .do/app.yaml

The included .do/app.yaml deploys the gateway in standalone mode using the pre-built GHCR image. Set your provider API keys in the App Platform dashboard under Settings → Environment Variables.

Custom App Spec

For a multi-service deployment with the DVARA Flightdeck:

# .do/app.yaml
spec:
name: dvara
region: nyc
services:
- name: gateway-server
image:
registry_type: GHCR
registry: dvarahq
repository: dvara/dvara-llm-gateway
tag: 1.0.0
http_port: 8080
instance_count: 1
instance_size_slug: professional-xs
health_check:
http_path: /actuator/health
initial_delay_seconds: 30
period_seconds: 15
envs:
- key: JAVA_OPTS
scope: RUN_TIME
value: "-Xmx768m"
- key: OPENAI_API_KEY
scope: RUN_TIME
type: SECRET
- key: ANTHROPIC_API_KEY
scope: RUN_TIME
type: SECRET

- name: flight-deck
image:
registry_type: GHCR
registry: dvarahq
repository: dvara/dvara-flightdeck
tag: 1.0.0
http_port: 8090
instance_count: 1
instance_size_slug: basic-s
health_check:
http_path: /actuator/health/liveness
initial_delay_seconds: 30
envs:
- key: DVARA_FLIGHTDECK_GATEWAY_URL
scope: RUN_TIME
value: "${gateway-server.PRIVATE_URL}"

Database for Enterprise Persistence

PostgreSQL is required for all DVARA deployments. Rate limiting and API-key caching run in-process — no external cache infrastructure is required.

Managed PostgreSQL:

doctl databases create dvara-db \
--engine pg --version 14 \
--size db-s-2vcpu-4gb \
--region nyc1 --num-nodes 2

Add the database connection details as environment variables in the App Platform dashboard, or attach them as components in the app spec:

envs:
- key: SPRING_DATASOURCE_URL
scope: RUN_TIME
value: "${dvara-db.DATABASE_URL}"

Scaling

App Platform supports horizontal scaling via the dashboard or CLI:

# Scale to 3 instances
doctl apps update <app-id> --spec <updated-spec-with-instance_count-3>

For production, use professional-xs or larger instance sizes which provide dedicated CPU resources.


Monitoring Stack

All cloud providers can use the same monitoring setup. Install the kube-prometheus-stack for Prometheus + Grafana:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring --create-namespace \
--set grafana.adminPassword=admin

Enable ServiceMonitor in DVARA:

gatewayServer:
serviceMonitor:
enabled: true
interval: 15s
additionalLabels:
release: monitoring

mcpProxyServer:
serviceMonitor:
enabled: true
interval: 15s
additionalLabels:
release: monitoring

Import the included Grafana dashboard from grafana/dashboards/ for pre-built panels covering request rates, latencies, token usage, provider health, cost metrics, and agent session stats.

Key Prometheus Metrics to Alert On

# Example alerting rules
groups:
- name: dvara
rules:
- alert: HighErrorRate
expr: rate(gateway_requests_total{status=~"5.."}[5m]) / rate(gateway_requests_total[5m]) > 0.05
for: 5m
labels:
severity: critical
- alert: HighLatency
expr: histogram_quantile(0.95, rate(gateway_latency_seconds_bucket[5m])) > 5
for: 5m
labels:
severity: warning
- alert: BudgetCapBreached
expr: increase(gateway_budget_blocked_total[1h]) > 0
labels:
severity: warning

Enterprise Features on Cloud

Vault Integration

DVARA supports HashiCorp Vault, AWS Secrets Manager, and Azure Key Vault as credential backends for LLM provider keys. This is separate from Kubernetes secret management — it provides runtime credential resolution for the gateway itself.

HashiCorp Vault (any cloud):

gatewayServer:
env:
- name: DVARA_VAULT_BACKEND
value: hashicorp
- name: VAULT_ADDR
value: "https://vault.internal:8200"
- name: VAULT_AUTH_METHOD
value: approle
- name: VAULT_ROLE_ID
valueFrom:
secretKeyRef:
name: vault-credentials
key: role-id
- name: VAULT_SECRET_ID
valueFrom:
secretKeyRef:
name: vault-credentials
key: secret-id

AWS Secrets Manager:

gatewayServer:
env:
- name: DVARA_VAULT_BACKEND
value: aws-secrets-manager
- name: AWS_VAULT_REGION
value: us-east-1
- name: AWS_SECRET_NAME
value: dvara/provider-credentials
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/dvara-secrets

Azure Key Vault:

gatewayServer:
env:
- name: DVARA_VAULT_BACKEND
value: azure-key-vault
- name: AZURE_VAULT_URL
value: "https://dvara-vault.vault.azure.net"
- name: AZURE_CLIENT_ID
value: "<managed-identity-client-id>"

OIDC / SSO Setup

Enable OIDC-based authentication for the admin API:

gatewayServer:
env:
- name: DVARA_FLIGHTDECK_SECURITY_ENABLED
value: "true"
- name: DVARA_FLIGHTDECK_OIDC_ISSUER_URI
value: "https://login.microsoftonline.com/<tenant-id>/v2.0" # Azure AD
# value: "https://accounts.google.com" # Google
# value: "https://cognito-idp.us-east-1.amazonaws.com/<pool>" # AWS Cognito
- name: DVARA_FLIGHTDECK_OIDC_AUDIENCE
value: "dvara-gateway"
- name: DVARA_FLIGHTDECK_OIDC_ROLE_CLAIM
value: "roles" # or "realm_access.roles" for Keycloak

Database Requirements

PostgreSQL is required for all DVARA deployments — the gateway fails fast at startup if spring.datasource.url is not set. Rate limiting and the distributed cache for API-key lookups both run in-process — no external cache infrastructure is needed.

DependencyMinimum VersionPurpose
PostgreSQL14+Required. Audit events, policies, cost records, token usage, tenants, API keys, routes, etc.

Database migrations run automatically on first startup. No manual schema setup is required. Subsequent application upgrades apply incremental migrations without data loss.

Connection pool defaults: the gateway data plane uses maximumPoolSize=2 (1 config read + 1 audit write — override via DVARA_DB_POOL_SIZE). Flightdeck and bulk-load jobs default to maximumPoolSize=10. For high-throughput deployments, scale the relevant pool via SPRING_DATASOURCE_HIKARI_MAXIMUM_POOL_SIZE.


Quick Reference: Cloud-Specific Values

SettingAWSGCPAzureDigitalOcean
Ingress classalbgcenginxApp Platform (built-in)
TLS terminationACM certificate ARN annotationGKE ManagedCertificatecert-manager + Let's EncryptAutomatic (App Platform)
Service account identityIRSA (eks.amazonaws.com/role-arn)Workload Identity (iam.gke.io/gcp-service-account)Managed IdentityN/A
Secret managementSecrets Manager + External SecretsSecret Manager + External SecretsKey Vault + CSI DriverApp Platform env vars (encrypted)
Node type (recommended)m6i.large (2 vCPU, 8 GB)e2-standard-4 (4 vCPU, 16 GB)Standard_D4s_v3 (4 vCPU, 16 GB)professional-xs (1 vCPU, 1 GB dedicated)
AutoscalerCluster Autoscaler or KarpenterGKE Autopilot or Cluster AutoscalerAKS Cluster AutoscalerManual instance count
Managed PostgreSQL (required)RDS for PostgreSQLCloud SQL for PostgreSQLAzure Database for PostgreSQLDigitalOcean Managed Databases

Production Checklist

Before going to production on any cloud provider:

  • Enable HPA with appropriate min/max replicas
  • Enable PDB with minAvailable >= 2
  • Configure topology spread constraints for cross-zone scheduling
  • Use external secret management (not Helm --set for API keys)
  • Enable Ingress with TLS termination
  • Enable ServiceMonitor for Prometheus scraping
  • Set JVM options: -Xms512m -Xmx768m (adjust for workload)
  • Configure terminationGracePeriodSeconds: 45 and preStopSleepSeconds: 5
  • Set maxSurge: 1 and maxUnavailable: 0 for zero-downtime deploys
  • Provision PostgreSQL 14+ (required) — no other external cache infrastructure is needed
  • Enable enterprise license key for governance features
  • Set up alerting on error rate, latency, and budget metrics
  • Configure OIDC/SSO for admin API access
  • Enable audit HMAC signing with a production secret
  • Review and set budget caps for cost governance