Skip to main content
Version: 1.7.0

Kubernetes Deployment

Deploy DVARA on Kubernetes with the official Helm chart.

Prerequisites

  • Kubernetes 1.28+
  • Helm 3.x
  • kubectl configured for your cluster
  • A reachable PostgreSQL 14+ instance (the chart does NOT bundle PostgreSQL — see Common Deployment Patterns below for how to wire it)
  • A DVARA license envelope (DVARA-… prefix), optional. Everything boots without one, in the Development posture; a license activates the MCP and A2A planes. Refuse to start without it. Enterprise also needs the private -ee / proxy images (see Editions & container images) — no operator-flippable license bypass.

Quick Start

Mint the required secrets first — every DVARA install needs five chart-managed secrets:

# 5 chart-managed secrets
export DVARA_LICENSE_KEY="DVARA-…" # from your trial / contract email
export ACTUATOR_API_KEY=$(openssl rand -base64 32) # operator Bearer for /actuator/gateway-status
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

Pre-built images and the Helm chart are published to GitHub Container Registry:

helm install dvara oci://ghcr.io/dvarahq/charts/dvara \
--version 1.7.0 \
--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.mockProviderEnabled=true

# 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, pointing at a local chart directory:

helm install dvara charts/dvara/ \
--version 1.7.0 \
--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.mockProviderEnabled=true

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.

All five chart-managed secrets are required at boot

Every pod boots without enterpriseLicenseKey — nothing hard-fails on a missing license as of 1.7.0. An unlicensed install runs in the Development posture and serves the complete product except the MCP and A2A planes, which simply do not activate. (There are no separate MCP or A2A pods to fail: since 1.7.0 all three planes run inside the gateway process.) The actuator chain returns 401 on every authenticated endpoint if the two API keys are unset. ENCRYPTED-mode credential persistence fails if the encryption master password is unset. The audit chain refuses to write on a production-class profile if the HMAC secret is unset or carries the default placeholder.

Installing with Provider Keys

helm install dvara charts/dvara/ \
--version 1.7.0 \
--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.7.0 -f my-values.yaml

Configuration Reference

Gateway Server

ParameterDescriptionDefault
llmGatewayServer.enabledEnable gateway-servertrue
llmGatewayServer.replicaCountReplicas (ignored when HPA enabled)1
llmGatewayServer.image.repositoryImage repositoryghcr.io/dvarahq/dvara-gateway
llmGatewayServer.image.tagImage tag (defaults to chart appVersion)""
llmGatewayServer.region.idRegion identity for multi-region deployments""
llmGatewayServer.region.nameHuman-readable region name""
llmGatewayServer.javaOptsJVM options""
llmGatewayServer.resources.requests.cpuCPU request250m
llmGatewayServer.resources.requests.memoryMemory request512Mi
llmGatewayServer.resources.limits.cpuCPU limit2
llmGatewayServer.resources.limits.memoryMemory limit1Gi
llmGatewayServer.service.typeService typeClusterIP
llmGatewayServer.service.portService port8080

Flightdeck (Console + Portal)

The Helm chart's flightdeck.* parameter family configures the DVARA Flightdeck pod (Console + workspace Portal); the deployed image is ghcr.io/dvarahq/dvara-flightdeck.

ParameterDescriptionDefault
flightdeck.enabledEnable flightdecktrue
flightdeck.replicaCountNumber of replicas1
flightdeck.image.repositoryImage repositoryghcr.io/dvarahq/dvara-flightdeck
flightdeck.llmGatewayServerUrlOverride auto-discovered server URL"" (auto)
flightdeck.resources.requests.cpuCPU request100m
flightdeck.resources.requests.memoryMemory request256Mi
flightdeck.service.typeService typeClusterIP
flightdeck.email.transportEmail transport — log, smtp, or resend. Unset renders no email config at all""
flightdeck.email.fromSender address on every outbound""
flightdeck.email.hostSMTP server hostname. Required when transport=smtp — the pod refuses to start without it""
flightdeck.email.portSMTP port""
flightdeck.email.usernameSMTP username, when the server requires auth""
flightdeck.email.existingSecretName of an existing Secret holding the SMTP / Resend credential. Preferred over password""
flightdeck.email.existingSecretKeyKey within existingSecretemail-password
flightdeck.email.passwordCredential inline. Lands in helm get values output — prefer existingSecret""
flightdeck.service.portService port8090

Flightdeck Health Probes:

ProbePathPurpose
Liveness/actuator/health/livenessBasic liveness check
Readiness/actuator/health/readinessIncludes controlPlane check (gateway-server connectivity)
Startup/actuator/health/livenessAllows 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 k8s probes work without secrets. Do not set management.endpoint.health.show-details=always — the gateway refuses to start in that mode so anonymous callers cannot read per-indicator JSON like cache-cluster state or database pool internals. Leave the default when-authorized.

Secrets

ParameterDescriptionDefault
secrets.createCreate the Secret resourcetrue
secrets.existingSecretUse an existing Secret instead""
secrets.providerKeys.openaiOpenAI API key""
secrets.providerKeys.anthropicAnthropic API key""
secrets.providerKeys.geminiGemini API key""
secrets.providerKeys.awsAccessKeyIdAWS access key (Bedrock)""
secrets.providerKeys.awsSecretAccessKeyAWS secret key (Bedrock)""
secrets.ollamaEnabledEnable Ollama provider""
secrets.ollamaBaseUrlOllama base URL""
secrets.bedrockEnabledEnable Bedrock provider""
secrets.mockProviderEnabledEnable mock provider""
secrets.auditHmacSecretHMAC-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.gatewayEncryptionMasterPasswordAES-256-GCM master password for ENC: values + ENCRYPTED-mode provider credentials""
secrets.llmGatewayServerApiKeyOperator Bearer for /actuator/gateway-status + every authenticated /actuator/* path EXCEPT prometheus. Generate with openssl rand -base64 32. Required — every authenticated actuator probe 401s without it.""
secrets.gatewayMetricsApiKeyDistinct Bearer for /actuator/prometheus only. Must differ from llmGatewayServerApiKey (principle of least privilege — a leaked scrape token must not unlock the license envelope). Required.""
secrets.enterpriseLicenseKeyDVARA license envelope (DVARA-… prefix, Ed25519-signed). Activates the MCP and A2A planes and confers production rights. Every pod boots without it, in the Development posture, serving everything else. No -ee images are needed — there is one artifact per app.""
Audit HMAC secret

secrets.auditHmacSecretDVARA_AUDIT_HMAC_SECRET signs every row in the audit chain. Since 1.7.0 leaving it unset is safe where a database is configured — the install generates a secret on first boot and persists it, shared across pods. What boot still refuses on a production-class profile is the default-dev-secret-change-in-production placeholder, which is public. Set it explicitly to hold the key material yourself, and always set it — the same value on both halves — where a data-plane pod ships audit to Flightdeck, since the pod signs and Flightdeck verifies without re-signing. Set it via --set secrets.auditHmacSecret=… or through secrets.existingSecret (key audit-hmac-secret).

The dataPlane block

A top-level block, shared by the data-plane workload, that configures the signed config-bundle channel. It renders nothing at all unless dataPlane.controlPlane.baseUrl is set, so an install that is not cutting over is unchanged by its presence.

valuewhat it does
dataPlane.controlPlane.baseUrlthe control plane to fetch bundles from. Setting this is what turns the channel on.
dataPlane.controlPlane.publicKeythe Ed25519 key that verifies a bundle. Required — the render fails without it rather than deploying a pod that fetches bundles it cannot verify.
dataPlane.serveFromBundlewhether the pod actually reads from the bundle. Default false — see the two switches.
dataPlane.tls.secretNamethe /internal client certificate.
dataPlane.dbMigratedefault true. Set false to make Flightdeck the sole migrator — then Flightdeck must migrate before these pods start, so order them behind its readiness.

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.

Datasource credentials are not part of this block — supply SPRING_DATASOURCE_* through 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:

valueimagelicence
valkey.enabledvalkey/valkey-bundleBSD-3
redis.enabledredis:8tri-licensed RSALv2 / SSPLv1 / AGPLv3

Valkey is the documented 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.

If your organisation restricts AGPL software, use Valkey or an external managed instance. This is not legal advice — consult your own counsel.

Single-node, for evaluation

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 whenever the license permits, 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: apply a license and address /mcp/* and /a2a/* on the gateway Service.

Ingress

ParameterDescriptionDefault
ingress.enabledEnable Ingressfalse
ingress.classNameIngress class (nginx, traefik, alb)""
ingress.annotationsIngress annotations{}
ingress.llmGatewayServer.hostsServer host/path rules[{host: gateway.example.com}]
ingress.llmGatewayServer.tlsServer TLS config[]
ingress.flightdeck.hostsUI host/path rules[{host: admin.example.com}]
ingress.flightdeck.tlsUI TLS config[]

Graceful Shutdown & Rolling Updates

ParameterDescriptionDefault
llmGatewayServer.terminationGracePeriodSecondsPod termination grace period (must exceed preStop + drain)45
llmGatewayServer.preStopSleepSecondsSleep before SIGTERM (endpoint de-registration propagation)5
llmGatewayServer.rollingUpdate.maxSurgeMax extra pods during rolling update1
llmGatewayServer.rollingUpdate.maxUnavailableMax unavailable pods during rolling update (0 = zero-downtime)0
llmGatewayServer.topologySpreadConstraintsTopology 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)

ParameterDescriptionDefault
llmGatewayServer.autoscaling.enabledEnable HPAfalse
llmGatewayServer.autoscaling.minReplicasMinimum replicas2
llmGatewayServer.autoscaling.maxReplicasMaximum replicas10
llmGatewayServer.autoscaling.targetCPUUtilizationPercentageCPU target70
llmGatewayServer.autoscaling.targetMemoryUtilizationPercentageMemory target80
llmGatewayServer.autoscaling.behavior.scaleUp.stabilizationWindowSecondsWait before scaling up30
llmGatewayServer.autoscaling.behavior.scaleDown.stabilizationWindowSecondsWait before scaling down300

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

ParameterDescriptionDefault
llmGatewayServer.pdb.enabledEnable PDBfalse
llmGatewayServer.pdb.minAvailableMin available pods1
llmGatewayServer.pdb.maxUnavailableMax unavailable pods""

Prometheus ServiceMonitor

ParameterDescriptionDefault
llmGatewayServer.serviceMonitor.enabledEnable (requires Prometheus Operator)false
llmGatewayServer.serviceMonitor.intervalScrape interval30s
llmGatewayServer.serviceMonitor.pathMetrics path/actuator/prometheus
llmGatewayServer.serviceMonitor.additionalLabelsLabels 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

DVARA LLM Gateway instances share rate-limit counters and API key lookups across the fleet. On Kubernetes, pods must discover each other via a headless Service — multicast is unavailable in most clusters.

When KUBERNETES_NAMESPACE is set (the downward API auto-injects this), the gateway requires CACHE_SERVICE_NAME to point at a headless Service fronting the gateway pods. Without it, startup fails with:

Kubernetes clustering requires CACHE_SERVICE_NAME when
KUBERNETES_NAMESPACE is set. Without it, pods cannot form a cluster and
rate limit state will not be shared.

The official Helm chart wires both variables automatically when you deploy multiple gateway replicas — you don't need the manual YAML below unless you're bypassing the chart. Outside Kubernetes (local docker compose, bare metal), gateway instances auto-discover each other via multicast and CACHE_SERVICE_NAME is not required.

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)

PostgreSQL is required even for minimal deployments — there is no in-memory fallback. For quick testing, point the chart at an external Postgres or deploy a small Postgres StatefulSet alongside the gateway:

helm install dvara charts/dvara/ \
--version 1.7.0 \
--set secrets.mockProviderEnabled=true \
--set llmGatewayServer.env.SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/dvara \
--set llmGatewayServer.env.SPRING_DATASOURCE_USERNAME=dvara \
--set llmGatewayServer.env.SPRING_DATASOURCE_PASSWORD=dvara

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.7.0 -f production-values.yaml

MCP and A2A governance

No extra values are needed. Supply a license and the planes are served by the gateway:

# licensed-values.yaml
secrets:
enterpriseLicenseKey: "DVARA-..." # activates the MCP and A2A planes

llmGatewayServer:
replicaCount: 3
helm install dvara oci://ghcr.io/dvarahq/charts/dvara --version 1.7.0 \
-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, gateway-server-api-key, gateway-metrics-api-key, audit-hmac-secret, enterprise-license-key (signed).

AWS Bedrock with IRSA

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

secrets:
bedrockEnabled: "true"

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.7.0 -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 filesystemreadOnlyRootFilesystem: true with a /tmp emptyDir for JVM temp files
  • No privilege escalationallowPrivilegeEscalation: false
  • Capabilities dropped — All Linux capabilities dropped
  • No service account tokenautomountServiceAccountToken: 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.7.0 -f my-values.yaml

# From local chart
helm upgrade dvara charts/dvara/ --version 1.7.0 -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 gateway-server, flightdeck, and (if enabled) mcp-proxy-server services are reachable.

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).