Kubernetes Deployment
Deploy DVARA on Kubernetes with the official Helm chart.
Prerequisites
- Kubernetes 1.28+
- Helm 3.x
kubectlconfigured for your cluster- A reachable PostgreSQL 14+ instance for Flightdeck (the chart does NOT bundle PostgreSQL — see Give Flightdeck its database below). From 1.8.2 the gateway has no database: only Flightdeck connects to PostgreSQL and migrates it, and gateways get their configuration from Flightdeck as a signed bundle. See How the gateway runs without a database.
- A DVARA licence envelope (
DVARA-…prefix) only for Enterprise production rights and support. You can add it later on Flightdeck's License page; see Licensing.
Quick Start
Mint the operational secrets first. Add the licence key when the deployment needs Enterprise production rights, and the PII token password when a workspace uses pii.action=TOKENIZE:
# Operational secrets, plus an optional licence envelope and PII token password
export DVARA_LICENSE_KEY="DVARA-…" # optional: copied into Flightdeck once
export ACTUATOR_API_KEY=$(openssl rand -base64 32) # operator Bearer for gateway status and Enterprise licence health
export METRICS_API_KEY=$(openssl rand -base64 32) # DISTINCT Bearer for /actuator/prometheus
export ENCRYPTION_PASSWORD=$(openssl rand -base64 32) # AES-256-GCM key for ENCRYPTED-mode credentials
export AUDIT_HMAC=$(openssl rand -base64 32) # signs audit-chain envelopes
export ENROLMENT_SECRET=$(openssl rand -base64 32) # gateways enrol with Flightdeck by it
export PII_TOKEN_PASSWORD=$(openssl rand -base64 32) # optional: Flightdeck seals PII token keys with it (TOKENIZE)
The chart generates the enrolment secret on install if you leave it empty, and keeps it across
upgrades. Set it explicitly when you render with helm template or Argo CD, which cannot read the
existing value back.
Give Flightdeck its database
Put the PostgreSQL connection in a values file. It goes on Flightdeck only: from 1.8.2 a gateway
refuses to start with a database configured, and helm upgrade refuses a values file that gives
the gateway SPRING_DATASOURCE_* through llmGatewayServer.extraEnv.
# db-values.yaml
flightdeck:
extraEnv:
- name: SPRING_DATASOURCE_URL
value: jdbc:postgresql://<postgres-host>:5432/dvara
- name: SPRING_DATASOURCE_USERNAME
value: dvara
- name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: dvara-db
key: password
From OCI Registry (recommended)
Pre-built images and the Helm chart are published to GitHub Container Registry:
helm install dvara oci://ghcr.io/dvarahq/charts/dvara \
--version 1.8.5 \
--set secrets.enterpriseLicenseKey="$DVARA_LICENSE_KEY" \
--set secrets.llmGatewayServerApiKey="$ACTUATOR_API_KEY" \
--set secrets.gatewayMetricsApiKey="$METRICS_API_KEY" \
--set secrets.gatewayEncryptionMasterPassword="$ENCRYPTION_PASSWORD" \
--set secrets.auditHmacSecret="$AUDIT_HMAC" \
--set secrets.enrolmentSharedSecret="$ENROLMENT_SECRET" \
--set secrets.piiTokenEncryptionMasterPassword="$PII_TOKEN_PASSWORD" \
--set secrets.mockProviderEnabled=true \
-f db-values.yaml
# Wait for pods to be ready
kubectl rollout status deployment/dvara-server
kubectl rollout status deployment/dvara-flightdeck
# Verify
kubectl port-forward svc/dvara-server 8080:8080 &
curl http://localhost:8080/actuator/health
{"status": "UP", "groups": ["liveness", "readiness"]}
That is the whole body, and it is deliberate: management.endpoint.health.show-details is
when-authorized, so an anonymous caller — which is what a probe and a curl both are — gets the
verdict and nothing else. Per-indicator detail would expose cluster and pool internals. If you need
it, authenticate; if you are debugging a pod that will not become ready, use
/actuator/health/readiness and the pod's logs rather than expecting more here.
From Local Chart
Same secret arguments and database values, pointing at a local chart directory:
helm install dvara charts/dvara/ \
--version 1.8.5 \
--set secrets.enterpriseLicenseKey="$DVARA_LICENSE_KEY" \
--set secrets.llmGatewayServerApiKey="$ACTUATOR_API_KEY" \
--set secrets.gatewayMetricsApiKey="$METRICS_API_KEY" \
--set secrets.gatewayEncryptionMasterPassword="$ENCRYPTION_PASSWORD" \
--set secrets.auditHmacSecret="$AUDIT_HMAC" \
--set secrets.enrolmentSharedSecret="$ENROLMENT_SECRET" \
--set secrets.piiTokenEncryptionMasterPassword="$PII_TOKEN_PASSWORD" \
--set secrets.mockProviderEnabled=true \
-f db-values.yaml
For any deployment past initial smoke-testing, move these into a values file (or an externally-managed Secret via secrets.existingSecret) rather than passing on the command line.
The Enterprise applications boot without enterpriseLicenseKey in the Development posture, with every plane serving. The actuator chain returns 401 on authenticated endpoints if its API keys are unset. ENCRYPTED credential storage fails without the encryption master password. The audit chain refuses to write on a production-class profile if its HMAC secret is unset or still uses the public development placeholder.
Installing with Provider Keys
helm install dvara charts/dvara/ \
--version 1.8.5 \
--set secrets.providerKeys.openai=sk-... \
--set secrets.providerKeys.anthropic=sk-ant-...
Or use a values file:
# my-values.yaml
secrets:
providerKeys:
openai: sk-...
anthropic: sk-ant-...
gemini: AIza...
helm install dvara charts/dvara/ --version 1.8.5 -f my-values.yaml
Configuration Reference
Gateway Server
| Parameter | Description | Default |
|---|---|---|
llmGatewayServer.enabled | Enable gateway-server | true |
llmGatewayServer.replicaCount | Replicas (ignored when HPA enabled) | 1 |
llmGatewayServer.image.repository | Image repository | ghcr.io/dvarahq/dvara-gateway |
llmGatewayServer.image.tag | Image tag (defaults to chart appVersion) | "" |
llmGatewayServer.region.id | Region identity for multi-region deployments | "" |
llmGatewayServer.region.name | Human-readable region name | "" |
llmGatewayServer.javaOpts | JVM options | "" |
llmGatewayServer.resources.requests.cpu | CPU request | 250m |
llmGatewayServer.resources.requests.memory | Memory request | 512Mi |
llmGatewayServer.resources.limits.cpu | CPU limit | 2 |
llmGatewayServer.resources.limits.memory | Memory limit | 1Gi |
llmGatewayServer.service.type | Service type | ClusterIP |
llmGatewayServer.service.port | Service port | 8080 |
llmGatewayServer.a2a.pushCallbackBaseUrl | From 1.8.5. The gateway's public URL as peer agents reach it, such as https://api.acme.example. Set on the gateway as A2A_PUSH_CALLBACK_BASE_URL. Needed for A2A push notifications. | "" |
From 1.8.5 A2A push notifications go through the gateway, which gives each peer
a callback under llmGatewayServer.a2a.pushCallbackBaseUrl. The chart does not
work it out from the ingress: set the URL peers really use. While it is empty,
every A2A push config is refused with 503 A2A_PUSH_NOT_AVAILABLE, except in a
workspace that keeps direct delivery (a2a.push.delivery=direct). A value you
set for A2A_PUSH_CALLBACK_BASE_URL in llmGatewayServer.extraEnv wins, with
no duplicate entry. See
Receive push notifications through DVARA.
Flightdeck
The Helm chart's flightdeck.* parameter family configures the DVARA Flightdeck pod; the deployed image is ghcr.io/dvarahq/dvara-flightdeck.
| Parameter | Description | Default |
|---|---|---|
flightdeck.enabled | Enable flightdeck | true |
flightdeck.replicaCount | Number of replicas | 1 |
flightdeck.image.repository | Image repository | ghcr.io/dvarahq/dvara-flightdeck |
flightdeck.llmGatewayServerUrl | Override auto-discovered server URL | "" (auto) |
flightdeck.resources.requests.cpu | CPU request | 100m |
flightdeck.resources.requests.memory | Memory request | 256Mi |
flightdeck.service.type | Service type | ClusterIP |
flightdeck.email.transport | Email transport — log, smtp, or resend. Unset renders no email config at all | "" |
flightdeck.email.from | Sender address on every outbound | "" |
flightdeck.email.host | SMTP server hostname. Required when transport=smtp — the pod refuses to start without it | "" |
flightdeck.email.port | SMTP port | "" |
flightdeck.email.username | SMTP username, when the server requires auth | "" |
flightdeck.email.existingSecret | Name of an existing Secret holding the SMTP / Resend credential. Preferred over password | "" |
flightdeck.email.existingSecretKey | Key within existingSecret | email-password |
flightdeck.email.password | Credential inline. Lands in helm get values output — prefer existingSecret | "" |
flightdeck.service.port | Service port | 8090 |
Flightdeck Health Probes:
| Probe | Path | Purpose |
|---|---|---|
| Liveness | /actuator/health/liveness | Basic liveness check |
| Readiness | /actuator/health/readiness | Checks Flightdeck application readiness and PostgreSQL; Gateway connectivity is excluded |
| Startup | /actuator/health/liveness | Allows JVM warmup before liveness kicks in |
All four probe paths (/actuator/health, /actuator/health/liveness, /actuator/health/readiness, /actuator/info) are anonymous by design so Kubernetes probes work without secrets. Do not set management.endpoint.health.show-details=always, or set management.endpoint.health.group.<name>.show-details=always for readiness, liveness, or a custom group. A group value overrides the endpoint value. In either case DVARA publishes REFUSING_TRAFFIC and its readiness probe fails so anonymous callers cannot read per-indicator details. Use when-authorized or never.
Use System Status for Gateway and provider health without coupling Flightdeck's Kubernetes readiness to the Gateway.
Secrets
| Parameter | Description | Default |
|---|---|---|
secrets.create | Create the Secret resource | true |
secrets.existingSecret | Use an existing Secret instead | "" |
secrets.providerKeys.openai | OpenAI API key | "" |
secrets.providerKeys.anthropic | Anthropic API key | "" |
secrets.providerKeys.gemini | Gemini API key | "" |
secrets.providerKeys.awsAccessKeyId | AWS access key (Bedrock) | "" |
secrets.providerKeys.awsSecretAccessKey | AWS secret key (Bedrock) | "" |
secrets.ollamaEnabled | Enable Ollama provider | "" |
secrets.ollamaBaseUrl | Ollama base URL | "" |
secrets.bedrockEnabled | Enable Bedrock provider | "" |
secrets.mockProviderEnabled | Enable mock provider | "" |
secrets.auditHmacSecret | HMAC-SHA256 key signing the tamper-evident audit chain. Required on production-class profiles — boot refuses the default-dev-secret-change-in-production placeholder. Generate with openssl rand -base64 32. | "" |
secrets.gatewayEncryptionMasterPassword | AES-256-GCM master password for ENC: values + ENCRYPTED-mode provider credentials. Passed to the gateway and, from 1.8.4, to Flightdeck as DVARA_ENCRYPTION_MASTER_PASSWORD, from the same Secret key (gateway-encryption-master-password). Before 1.8.4 Flightdeck never got it, so it refused to store a BYOK provider credential on a standard chart install. A value you set in flightdeck.extraEnv wins. | "" |
secrets.piiTokenEncryptionMasterPassword | From 1.8.4. Master password Flightdeck seals each workspace's PII token key with, for pii.action=TOKENIZE. Passed only to Flightdeck, as DVARA_PII_TOKEN_ENCRYPTION_MASTER_PASSWORD; gateways fetch the keys from Flightdeck and need no copy. Keep it: a lost or changed value makes every PII token unreadable. Empty: TOKENIZE is unavailable and each workspace's pii.degraded-action applies (BLOCK by default); detection, LOG and REDACT need no key. A value you set in flightdeck.extraEnv wins. With secrets.existingSecret, the key is pii-token-encryption-master-password. | "" |
secrets.llmGatewayServerApiKey | Operator Bearer for /actuator/gateway-status, Enterprise /actuator/license, and every other authenticated actuator path except prometheus. Generate with openssl rand -base64 32. Required for those operator surfaces. | "" |
secrets.gatewayMetricsApiKey | Distinct Bearer for /actuator/prometheus only. It must differ from llmGatewayServerApiKey so a leaked scrape token cannot unlock gateway or Enterprise licence metadata. | "" |
secrets.enterpriseLicenseKey | DVARA license envelope (DVARA-… prefix, Ed25519-signed). Passed only to Flightdeck, which copies it into its licence store once, when none is stored; Gateway pods do not read it. A licence confers production rights and removes the 3-workspace and 100,000-call limits. Every pod boots without it, in the Development posture, serving every plane. You can paste the licence on Flightdeck's License page instead. No -ee images are needed — there is one artifact per app. | "" |
secrets.auditHmacSecret → DVARA_AUDIT_HMAC_SECRET signs every row in the audit chain. Set it. From 1.8.0 DVARA no longer generates or stores this secret. Give every gateway pod and Flightdeck the same value, because pods sign the chain and Flightdeck verifies it without re-signing. On a production Spring profile a pod refuses to start without it. The chart sets no profile, so on the default profile a pod starts with only a warning, and the chain has no key you control. An install upgrading from 1.7 that relied on a generated secret can recover it first; see Upgrade DVARA to 1.8. Set it via --set secrets.auditHmacSecret=… or through secrets.existingSecret (key audit-hmac-secret).
From 1.8.4 the gateway also derives the A2A delegation guard's signing key from this secret when dvara.a2a.delegation.signing-secret is unset. A secret shorter than 32 bytes, or a placeholder DVARA publishes, is then refused: on a production profile the gateway does not start, and on a development profile, including the chart's default, it logs a warning and runs the guard as off. openssl rand -base64 32 gives 44 characters, which is enough.
The dataPlane block and the gateway's volume
From 1.8.2 the gateway has no database. It enrols with Flightdeck, reads its configuration from the signed bundle Flightdeck publishes, and spools audit, usage and cost to its data volume, which it ships to Flightdeck. The chart runs all of this by default; none of the values below needs to be set. See How the gateway runs without a database.
| value | what it does | default |
|---|---|---|
dataPlane.controlPlane.baseUrl | Flightdeck's /internal address. Empty means the <release>-flightdeck-internal Service on port 8443 in this release. Set it when Flightdeck runs elsewhere. | "" |
dataPlane.controlPlane.publicKey | The Ed25519 key that verifies a bundle. Optional: an enrolled gateway learns it from Flightdeck. A key you set always wins. | "" |
dataPlane.enrolment.enabled | Gateways enrol with Flightdeck using secrets.enrolmentSharedSecret and get their /internal certificate from it, renewed automatically. Turn it off only to supply certificates yourself through dataPlane.tls. | true |
dataPlane.tls.secretName | An operator-made /internal client certificate, used with dataPlane.enrolment.enabled: false. | "" |
flightdeck.enrolment.port | The port Flightdeck serves enrolment and /internal on, through its -internal Service. | 8443 |
secrets.enrolmentSharedSecret | The secret gateways enrol by, at least 32 characters. Empty: generated on install and kept across upgrades. With secrets.existingSecret, that Secret must carry the key enrolment-shared-secret. | "" |
llmGatewayServer.persistence.enabled | Give each gateway pod its own volume claim. Turns the gateway from a Deployment into a StatefulSet. Recommended for production. | false |
llmGatewayServer.persistence.mountPath | Where the data volume is mounted. The spool, the bundle snapshot and the enrolled certificate live under it. With persistence off it is an emptyDir. | /var/lib/dvara |
llmGatewayServer.persistence.size | Volume size per replica | 10Gi |
llmGatewayServer.persistence.shutdownDrainSeconds | Seconds a pod spends flushing its spool on SIGTERM | 20 |
dataPlane.tls.secretName sets both SSL properties, which is the part that is easy to get wrong by hand: one carries the client certificate and the other only checks the CN. Setting the second alone gives a pod that starts and then fails every fetch. The chart removes that choice.
dataPlane.serveFromBundle and dataPlane.dbMigrate were removed in 1.8.2. A values file that still sets either one fails helm upgrade with the setting named. Database credentials go on Flightdeck only, through flightdeck.extraEnv.
The bundled cache carries a licensing choice
The chart can deploy a single-node cache for evaluation. Both are disabled by default, and which one you enable is a licensing decision, not a preference:
| value | image | licence |
|---|---|---|
valkey.enabled | valkey/valkey-bundle | BSD-3 |
redis.enabled | redis:8 | tri-licensed RSALv2 / SSPLv1 / AGPLv3 |
Valkey is the recommended choice when you select an external cache. Neither cache is enabled by default. BSD-3 raises no distribution question. Redis Open Source 8+ is AGPLv3 among its options — OSI-approved open source, and copyleft with a network clause, which is a materially different position from the BSD licence Redis carried before March 2024. Enabling redis pulls an AGPL program from its publisher's registry into your cluster; DVARA ships a template naming an image, not the image itself.
Enabling valkey.enabled or redis.enabled only deploys the evaluation cache. It does not point the Gateway at it. Set DVARA_LLM_GATEWAY_RATE_LIMIT_SHARED_CACHE_URL separately through llmGatewayServer.extraEnv; without that URL, the cache pod is idle and the normal limiter remains active. See Control request and token throughput.
If your organisation restricts AGPL software, use Valkey or an external managed instance. This is not legal advice — consult your own counsel.
Neither profile is a production topology. Point production at a managed offering — ElastiCache for Valkey, Memorystore, Azure Cache.
MCP and A2A planes
There are no mcpGatewayServer or a2aGatewayServer values to set. Both planes run inside the LLM gateway and are served by it, with or without a license, on the same port as /v1/*. The
separate dvara-mcp-gateway and dvara-a2a-gateway images are no longer published and their
workload templates are gone.
The chart refuses to render if you enable either — a hard failure naming the replacement, rather than silently giving you a smaller cluster than you asked for with the MCP plane quietly not served.
To use them: address /mcp/* and /a2a/* on the gateway Service.
Ingress
| Parameter | Description | Default |
|---|---|---|
ingress.enabled | Enable Ingress | false |
ingress.className | Ingress class (nginx, traefik, alb) | "" |
ingress.annotations | Ingress annotations | {} |
ingress.llmGatewayServer.hosts | Server host/path rules | [{host: gateway.example.com}] |
ingress.llmGatewayServer.tls | Server TLS config | [] |
ingress.flightdeck.hosts | UI host/path rules | [{host: admin.example.com}] |
ingress.flightdeck.tls | UI TLS config | [] |
Graceful Shutdown & Rolling Updates
| Parameter | Description | Default |
|---|---|---|
llmGatewayServer.terminationGracePeriodSeconds | Pod termination grace period (must exceed preStop + drain) | 45 |
llmGatewayServer.preStopSleepSeconds | Sleep before SIGTERM (endpoint de-registration propagation) | 5 |
llmGatewayServer.rollingUpdate.maxSurge | Max extra pods during rolling update | 1 |
llmGatewayServer.rollingUpdate.maxUnavailable | Max unavailable pods during rolling update (0 = zero-downtime) | 0 |
llmGatewayServer.topologySpreadConstraints | Topology spread for cross-zone scheduling | [] |
The default configuration ensures zero-downtime rolling updates: maxSurge: 1 creates one new pod before terminating old ones, and maxUnavailable: 0 ensures at least N pods are always ready. The preStopSleepSeconds delay allows Kubernetes endpoint propagation to complete before the application receives SIGTERM and begins its 30-second graceful drain.
Autoscaling (HPA)
| Parameter | Description | Default |
|---|---|---|
llmGatewayServer.autoscaling.enabled | Enable HPA | false |
llmGatewayServer.autoscaling.minReplicas | Minimum replicas | 2 |
llmGatewayServer.autoscaling.maxReplicas | Maximum replicas | 10 |
llmGatewayServer.autoscaling.targetCPUUtilizationPercentage | CPU target | 70 |
llmGatewayServer.autoscaling.targetMemoryUtilizationPercentage | Memory target | 80 |
llmGatewayServer.autoscaling.behavior.scaleUp.stabilizationWindowSeconds | Wait before scaling up | 30 |
llmGatewayServer.autoscaling.behavior.scaleDown.stabilizationWindowSeconds | Wait before scaling down | 300 |
The default HPA behavior scales up quickly (50% per minute after 30s stabilization) but scales down conservatively (25% per 2 minutes after 5-minute stabilization) to prevent flapping.
Pod Disruption Budget
| Parameter | Description | Default |
|---|---|---|
llmGatewayServer.pdb.enabled | Enable PDB | false |
llmGatewayServer.pdb.minAvailable | Min available pods | 1 |
llmGatewayServer.pdb.maxUnavailable | Max unavailable pods | "" |
Prometheus ServiceMonitor
| Parameter | Description | Default |
|---|---|---|
llmGatewayServer.serviceMonitor.enabled | Enable (requires Prometheus Operator) | false |
llmGatewayServer.serviceMonitor.interval | Scrape interval | 30s |
llmGatewayServer.serviceMonitor.path | Metrics path | /actuator/prometheus |
llmGatewayServer.serviceMonitor.additionalLabels | Labels for monitor selection | {} |
When llmGatewayServer.serviceMonitor.enabled=true, configure the ServiceMonitor's bearerTokenFile (or the Helm equivalent) to point at a file containing the DVARA_ACTUATOR_METRICS_API_KEY value. /actuator/prometheus is authenticated — without the token, every scrape returns 401 and the time series goes dark. The metrics secret is intentionally distinct from DVARA_ACTUATOR_API_KEY so a leaked scrape token can't unlock the rich gateway status surface.
Clustering on Kubernetes
The Enterprise embedded cluster shares rate-limit counters, response-cache entries, and API-key lookups across Gateway pods. On Kubernetes, those pods must discover each other through a headless Service because multicast is unavailable in most clusters. The Open Source distribution does not gain shared rate limits from this Service; each Open Source Gateway process keeps its own token bucket.
When KUBERNETES_NAMESPACE is set (the downward API auto-injects this), set CACHE_SERVICE_DNS to the DNS name of the headless Service fronting the gateway pods. The official chart sets it automatically. CACHE_SERVICE_NAME remains the lower-level service-name input for deployments that construct the DNS name themselves.
This DNS discovery mode does not query the Kubernetes API, so it needs no
Role or RoleBinding. The headless Service must expose the cluster port
5701. After rollout, confirm gateway_hazelcast_cluster_members equals the
Gateway replica count on every pod.
Embedded-cluster discovery is not configured; this pod will use its local member only.
Rate-limit and cache state will not be shared across replicas.
Missing discovery configuration produces a warning rather than stopping the process. Treat that warning as a deployment failure when you run more than one replica: each pod otherwise admits its own rate limit. You do not need the manual YAML below unless you bypass the chart. Outside Kubernetes, the Enterprise embedded cluster uses multicast discovery.
Headless Service pattern (reference)
apiVersion: v1
kind: Service
metadata:
name: dvara-server-cluster
labels:
app.kubernetes.io/name: dvara-server
spec:
clusterIP: None # headless — each pod gets a DNS A record
publishNotReadyAddresses: true
selector:
app.kubernetes.io/name: dvara-server
ports:
- name: cluster
port: 5701
targetPort: 5701
Deployment environment variables (reference)
apiVersion: apps/v1
kind: Deployment
metadata:
name: dvara-server
spec:
template:
spec:
containers:
- name: gateway-server
env:
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: CACHE_SERVICE_NAME
value: dvara-server-cluster
Common Deployment Patterns
Minimal Mode (Testing)
Flightdeck needs PostgreSQL even for minimal deployments — there is no in-memory fallback. The gateway needs none. For quick testing, point Flightdeck at an external Postgres or deploy a small Postgres StatefulSet alongside it, and pass the connection in db-values.yaml:
helm install dvara charts/dvara/ \
--version 1.8.5 \
--set secrets.auditHmacSecret="$AUDIT_HMAC" \
--set secrets.mockProviderEnabled=true \
-f db-values.yaml
Production with OpenAI
# production-values.yaml
llmGatewayServer:
replicaCount: 3
# Match heap to container limits below — leave ~25% of the limit for
# off-heap (Metaspace, native code, JIT, kernel buffers). With memory
# limit = 2Gi, -Xmx1500m is a safe upper bound; reserve more if you
# see Metaspace pressure in JFR.
javaOpts: "-Xms1g -Xmx1500m"
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
terminationGracePeriodSeconds: 45
preStopSleepSeconds: 5
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
pdb:
enabled: true
minAvailable: 2
secrets:
providerKeys:
openai: sk-...
ingress:
enabled: true
className: nginx
llmGatewayServer:
hosts:
- host: gateway.mycompany.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: gateway-tls
hosts:
- gateway.mycompany.com
helm install dvara charts/dvara/ --version 1.8.5 -f production-values.yaml
MCP and A2A governance
No extra values are needed. The planes are served by the gateway with or without a license. For production, supply a license:
# licensed-values.yaml
secrets:
enterpriseLicenseKey: "DVARA-..." # production rights; copied into Flightdeck once
llmGatewayServer:
replicaCount: 3
helm install dvara oci://ghcr.io/dvarahq/charts/dvara --version 1.8.5 \
-n dvara --create-namespace -f licensed-values.yaml
/mcp/* and /a2a/* are then served on the gateway Service alongside /v1/*.
Inline Gateway Configuration
Pass a gateway.yaml configuration directly via Helm values:
llmGatewayServer:
gatewayConfig:
routing:
default-strategy: round-robin
rate-limit:
enabled: true
per-key:
requests-per-minute: 100
This creates a ConfigMap mounted into the gateway pod and applied as Spring Boot externalized configuration.
Using External Secrets
If you manage secrets with External Secrets Operator, Sealed Secrets, or a vault:
secrets:
create: false
existingSecret: my-external-secret
The existing Secret must contain the same keys: openai-api-key, anthropic-api-key, gemini-api-key, aws-access-key-id, aws-secret-access-key, ollama-enabled, ollama-base-url, bedrock-enabled, mock-provider-enabled, gateway-encryption-master-password, pii-token-encryption-master-password (optional, from 1.8.4), gateway-server-api-key, gateway-metrics-api-key, audit-hmac-secret, enterprise-license-key (signed).
AWS Bedrock with explicit credentials
secrets:
bedrockEnabled: "true"
providerKeys:
awsAccessKeyId: <aws-access-key-id>
awsSecretAccessKey: <aws-secret-access-key>
DVARA 1.8 signs Bedrock requests with the explicit access-key and secret-key
pair. It does not use IRSA, an instance profile, container credentials, or the
AWS SDK default credential chain. Use secrets.existingSecret to keep the pair
in an externally managed Kubernetes Secret instead of Helm values.
Multi-Region Deployment
Deploy an instance in a specific region:
# us-east-values.yaml
llmGatewayServer:
region:
id: us-east-1
name: US East
secrets:
providerKeys:
openai: sk-...
helm install dvara-us-east charts/dvara/ --version 1.8.5 -f us-east-values.yaml
When region.id is set, the chart adds DVARA_REGION_ID and DVARA_REGION_NAME environment variables (which bind to the region identity configuration) and a dvara.ai/region pod label for topology-aware scheduling.
Prometheus Monitoring
llmGatewayServer:
serviceMonitor:
enabled: true
interval: 15s
additionalLabels:
release: prometheus-stack
The ServiceMonitor scrapes /actuator/prometheus. Requires the Prometheus Operator CRDs to be installed and a bearerTokenFile (or Helm-managed equivalent) pointing at the DVARA_ACTUATOR_METRICS_API_KEY value — see the ServiceMonitor parameter table above.
Security
The chart applies security hardening by default:
- Non-root execution — Pods run as UID 1001 (
runAsNonRoot: true) - Read-only filesystem —
readOnlyRootFilesystem: truewith a/tmpemptyDir for JVM temp files - No privilege escalation —
allowPrivilegeEscalation: false - Capabilities dropped — All Linux capabilities dropped
- No service account token —
automountServiceAccountToken: false(no K8s API access needed) - Secret key refs optional — Pods start even if only some provider keys are configured
Upgrading
# From OCI registry
helm upgrade dvara oci://ghcr.io/dvarahq/charts/dvara --version 1.8.5 -f my-values.yaml
# From local chart
helm upgrade dvara charts/dvara/ --version 1.8.5 -f my-values.yaml
Pods automatically restart when secrets or ConfigMap content changes (via checksum annotations on the pod template).
Running Helm Tests
helm test dvara
This runs test pods that verify the gateway-server and flightdeck services are reachable. The LLM, MCP, and A2A planes share the gateway service.
Uninstalling
helm uninstall dvara
Troubleshooting
Pods stuck in CrashLoopBackOff
Check logs for JVM startup errors:
kubectl logs deployment/dvara-server
Common causes:
- Insufficient memory — increase
resources.limits.memory - Missing secret keys — verify the Secret exists:
kubectl get secret dvara -o yaml
Startup probe fails
The startup probe allows 60 seconds (5s initial + 12 retries x 5s) for JVM warmup. If your image is large or the node is slow, increase the startup probe:
llmGatewayServer:
startupProbe:
failureThreshold: 20
Services not reachable
# Check pod status
kubectl get pods -l app.kubernetes.io/component=gateway-server
# Check service endpoints
kubectl get endpoints dvara-server
# Port-forward to test directly
kubectl port-forward svc/dvara-server 8080:8080
curl http://localhost:8080/actuator/health
Providers not registering
Provider keys must be non-empty strings. Check the Secret:
kubectl get secret dvara -o jsonpath='{.data.openai-api-key}' | base64 -d
Empty string = provider disabled (this is expected for unused providers).