Skip to main content

Kubernetes (Helm)

Enterprise only

The Helm chart is available exclusively to Mastra Enterprise customers. It is distributed from a private registry: your Enterprise license is exchanged for short-lived pull credentials. Contact us to get an Enterprise license.

Deploy the Mastra platform — Mastra Server (your agent runtime) and Mastra Studio (the management UI) — on any Kubernetes cluster using the official mastra-projects Helm chart. The chart works on GKE, EKS, AKS, and generic or local clusters, and handles service exposure, TLS, and secret wiring for you.

The chart follows two design principles:

  • Bring your own images. You build your Mastra project image with mastra build and push it to a registry. The chart never builds images.
  • Bring your own data stores. You point the chart at your PostgreSQL database and, optionally, an S3-compatible object store. Nothing stateful is bundled.

This guide covers the Helm chart, which manages Deployments, Services, Ingress or Gateway resources, and secrets for you. To write the Kubernetes manifests yourself, or to run multiple server pods with shared pub/sub, see Kubernetes.

Before you begin
Direct link to Before you begin

You'll need:

  • A Kubernetes 1.27+ cluster and kubectl
  • Helm 3.8+ (OCI registry support)
  • A container registry your cluster can pull from
  • A PostgreSQL database reachable from the cluster
  • A Mastra Enterprise license key — required both to pull the chart and at runtime in production
  • For ingress mode: an ingress controller, or let the chart install one
  • For gateway mode: Gateway API CRDs and a Gateway controller (for example GKE's managed Gateway)

Deploy
Direct link to Deploy

warning

Configure authentication in your Mastra application before building and installing the image. The default loadBalancer mode creates externally reachable Services that may expose Server and Studio to the internet, depending on your cluster and cloud provider.

  1. Build your Mastra project and containerize the output. Pass --studio so the same image can serve both Server and Studio:

    mastra build --studio
    Dockerfile
    FROM node:22-slim
    WORKDIR /app
    # Install dependencies inside the image: native modules (for example libsql)
    # are platform-specific, so node_modules built on your machine must not be
    # copied into the image (see .dockerignore below).
    COPY .mastra/output/package.json .mastra/output/package-lock.json* ./
    RUN npm install --force --prefer-offline --no-audit --no-fund
    COPY .mastra/output ./
    EXPOSE 4111
    CMD ["node", "index.mjs"]
    .dockerignore
    .mastra/output/node_modules
    warning

    Don't COPY the host-built node_modules into the image. mastra build installs native modules for your local platform — an image built on an Apple Silicon Mac for an amd64 cluster will crash at startup with errors like Cannot find module '@libsql/linux-x64-gnu'. Installing dependencies in-image (as above) always matches the target platform.

    Push the image to your registry. If your machine's architecture differs from your cluster nodes (for example an arm64 Mac deploying to amd64 nodes), build with --platform:

    docker buildx build --platform linux/amd64 \
    -t your-registry/my-mastra-app:1.0.0 --push .

    See Mastra server for build details.

  2. Store your application secrets in the release namespace. The chart reads database, license, and model-provider credentials from a Secret you own:

    kubectl create namespace mastra
    kubectl create secret generic mastra-app-env -n mastra \
    --from-literal=DATABASE_URL='postgresql://user:pass@host:5432/mastra' \
    --from-literal=MASTRA_EE_LICENSE='<your-license-key>' \
    --from-literal=OPENAI_API_KEY='<provider-key>' \
    --from-literal=CLICKHOUSE_URL='https://your-instance.clickhouse.cloud:8443' \
    --from-literal=CLICKHOUSE_USERNAME='<clickhouse-username>' \
    --from-literal=CLICKHOUSE_PASSWORD='<clickhouse-password>'

    Include any other environment variables your agents need, such as model provider API keys. Omit the CLICKHOUSE_* variables unless you use ClickHouse for observability.

  3. Create a values file pointing the chart at your image and secret:

    mastra-values.yaml
    global:
    cloud: generic # gke | eks | aks | generic | local

    mastra-server:
    image:
    repository: your-registry/my-mastra-app
    tag: '1.0.0'
    existingSecret: mastra-app-env

    mastra-studio:
    image:
    repository: your-registry/my-mastra-app
    tag: '1.0.0'
    existingSecret: mastra-app-env

    Both components reference the same Secret because this guide runs the same application image for Server and Studio. Only use separate Secrets when the Studio deployment doesn't execute routes that need your application's provider credentials.

    Set global.cloud to your platform so the chart emits the correct LoadBalancer and Ingress annotations for that cloud.

    If your cluster doesn't already have access to the application image registry, create an image pull Secret:

    kubectl create secret docker-registry mastra-registry -n mastra \
    --docker-server=your-registry \
    --docker-username='<username>' \
    --docker-password='<access-token>'

    Reference it from both components:

    mastra-values.yaml
    mastra-server:
    imagePullSecrets:
    - name: mastra-registry

    mastra-studio:
    imagePullSecrets:
    - name: mastra-registry

    Public images and registries integrated with your cluster don't need an image pull Secret.

    This is only the minimal configuration. After helm registry login, inspect the selected chart version's annotated defaults and README for every available value:

    CHART_VERSION=0.2.0
    CHART=oci://us-central1-docker.pkg.dev/mastra-cloud/mastra-helm-ee/mastra-projects

    helm show values "$CHART" --version "$CHART_VERSION"
    helm show readme "$CHART" --version "$CHART_VERSION"
  4. Exchange your Enterprise license key for a short-lived registry access token, log in to the private chart registry, then install the chart:

    CHART_VERSION=0.2.0
    CHART=oci://us-central1-docker.pkg.dev/mastra-cloud/mastra-helm-ee/mastra-projects

    TOKEN=$(curl -s https://license.mastra.ai/v1/registry-token \
    -H "Authorization: Bearer $MASTRA_EE_LICENSE" | jq -r .token)

    printf '%s' "$TOKEN" | helm registry login us-central1-docker.pkg.dev \
    --username oauth2accesstoken \
    --password-stdin

    helm install mastra "$CHART" \
    --version "$CHART_VERSION" \
    -n mastra -f mastra-values.yaml

    Tokens expire after a short period; if an upgrade later fails with an authorization error, request a fresh token and log in again.

    By default each component is exposed through a Service of type LoadBalancer.

  5. Verify the release. Wait for the pods, then check the server's health endpoint:

    kubectl -n mastra rollout status deployment -l app.kubernetes.io/instance=mastra
    kubectl -n mastra get svc
    curl http://<server-external-ip>:4111/health

    A {"success":true} response means the server is up. Open the Studio service address in a browser to reach the Mastra Studio UI.

Exposure modes
Direct link to Exposure modes

The chart supports three ways to expose Server and Studio, selected once via global.exposure.mode:

ModeWhat you getRequired values
loadBalancer (default)One LoadBalancer Service per component with per-cloud annotations
ingressClusterIP Services plus one Ingress per componentmastra-server.ingress.host, mastra-studio.ingress.host
gatewayClusterIP Services, a shared Gateway, and one HTTPRoute per componentmastra-server.httpRoute.host, mastra-studio.httpRoute.host

Ingress with Let's Encrypt TLS
Direct link to Ingress with Let's Encrypt TLS

Use ingress mode with cert-manager to serve both components over HTTPS. The chart can install ingress-nginx and cert-manager for you, or use controllers you already run:

mastra-values.yaml
global:
exposure:
mode: ingress
tls:
clusterIssuer: letsencrypt

ingressController:
install: true # omit if you already run an ingress controller

tls:
certManager:
install: true # omit if cert-manager is already installed
letsEncrypt:
email: ops@example.com

mastra-server:
ingress:
host: api.example.com

mastra-studio:
ingress:
host: studio.example.com

When tls.letsEncrypt.email is set, the chart renders a ClusterIssuer and annotates each Ingress so certificates are issued automatically. HTTP requests redirect to HTTPS.

On generic clusters the chart doesn't set an ingressClassName unless you configure one, so your cluster's default ingress class applies.

Gateway API
Direct link to Gateway API

Use gateway mode on clusters with Gateway API support. On GKE the chart selects the managed gke-l7-global-external-managed class automatically; on other clouds set global.gateway.className explicitly:

mastra-values.yaml
global:
exposure:
mode: gateway
tls:
clusterIssuer: letsencrypt

mastra-server:
httpRoute:
host: api.example.com

mastra-studio:
httpRoute:
host: studio.example.com

The chart creates a shared Gateway with HTTP and per-component HTTPS listeners. To attach to a Gateway you already run, set global.gateway.name instead.

Cloud presets
Direct link to Cloud presets

The chart ships a values preset per platform that sets global.cloud and platform-appropriate defaults:

PlatformPresetNotes
GKEvalues-gke.yamlGateway API recommended; Workload Identity supported via serviceAccount.annotations
EKSvalues-eks.yamlNLB by default; ALB via ingress annotations; IRSA supported
AKSvalues-aks.yamlAzure LoadBalancer annotations
Local (kind/minikube)values-local.yamlIngress on api.localhost / studio.localhost

Object storage
Direct link to Object storage

To connect an S3-compatible object store (S3, GCS with HMAC interoperability, or MinIO), set the generic object-store values on the server:

mastra-values.yaml
mastra-server:
externalServices:
objectStore:
endpoint: https://storage.googleapis.com
bucket: my-mastra-bucket
region: us-central1

Put S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY in your existing Secret rather than in values. On EKS and GKE, prefer IRSA or Workload Identity over static keys.

ClickHouse observability
Direct link to ClickHouse observability

To persist traces, logs, metrics, scores, and feedback in ClickHouse, add CLICKHOUSE_URL, CLICKHOUSE_USERNAME, and CLICKHOUSE_PASSWORD to the Secret that mastra-server.existingSecret references. The Secret example above includes those variables for ClickHouse Cloud.

The chart makes the variables available to your application. It doesn't configure observability or send telemetry by itself. Configure ObservabilityStorageClickhouseVNext for the observability storage domain and add MastraStorageExporter to your Mastra application. See ClickHouse for the complete application configuration.

When you enable mastra-workers, worker pods mount the Server's Secret. If you install the mastra-workers chart separately, set mastra-workers.server.existingSecret to the same Secret name. The mastra-projects chart verifies that the names match.

Scale server replicas
Direct link to Scale server replicas

The chart starts one Server replica by default. A HorizontalPodAutoscaler can add pods, but it doesn't make Mastra's in-process state available across them.

Before setting replicaCount above one or enabling mastra-server.autoscaling:

  • Configure a shared storage backend, such as PostgresStore, for persisted run state.
  • Configure a distributed PubSub backend and shared cache. The Kubernetes guide uses RedisStreamsPubSub and RedisServerCache, with REDIS_URL stored in the application Secret.
  • Use durable agents for streams, approvals, and runs that must continue when requests reach different pods.
  • Start with one replica to initialize the database schema before scaling. For stricter deployments, initialize the schema in a Kubernetes Job and disable initialization in each pod.
  • Review worker roles. Run only one scheduler instance when your application uses scheduled workflows.

After those requirements are in place, set resource requests and enable autoscaling:

mastra-values.yaml
mastra-server:
resources:
requests:
cpu: 250m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 80

Background workers
Direct link to Background workers

By default, the server runs Mastra's background workers in-process. The mastra-workers subchart moves them into their own deployments (one per role) so workflow orchestration, scheduled workflows, and background tasks scale independently of the API. See Workers for what each role does.

Workers are opt-in: nothing is deployed unless you enable them.

Requirements
Direct link to Requirements

Before enabling workers:

  • A pull-capable PubSub backend configured in your Mastra application: Redis Streams, Valkey Streams, or Google Cloud Pub/Sub. Workers pull events from the broker, so request/response-only backends won't work. Put its connection string (for example REDIS_URL) in the same Secret the Server uses.
  • A storage backend that supports the schedules domain, if you enable the scheduler: PostgreSQL, LibSQL, MySQL, MongoDB, Spanner, or Convex. Redis and ClickHouse don't support it, and the scheduler won't find due schedules on them.
  • mastra-server.workers.splitDeployment: true. Without it the API keeps running workers in-process and every worker runs twice, double-firing every cron schedule. The chart refuses to render if you enable workers without it.
Redis eviction policy

Set maxmemory-policy to noeviction (or a volatile-* policy) on Redis or Valkey. The common default allkeys-lru evicts unacknowledged workflow events under memory pressure, and runs then stall with no error on either side. On managed Redis this is a parameter-group setting.

Build the worker artifact into your image
Direct link to Build the worker artifact into your image

Workers run the same image as the Server, but a different entrypoint inside it. mastra build alone doesn't produce that entrypoint, so add a second build and redirect its output:

mastra build --studio
mastra worker build --output-dir .mastra/worker

--output-dir is required. Without it, mastra worker build writes to .mastra/output and overwrites the server bundle, leaving you with a worker-only image.

The two bundles declare different dependencies — the worker pulls in packages such as bufferutil and pg that the server bundle doesn't list — so each needs its own npm install. Node resolves modules upward from the entrypoint, so the worker finds its packages in .mastra/worker/node_modules:

Dockerfile
FROM node:22-slim
WORKDIR /app

# Server bundle and its dependencies.
COPY .mastra/output/package.json .mastra/output/package-lock.json* ./
RUN npm install --force --prefer-offline --no-audit --no-fund
COPY .mastra/output ./

# Worker bundle and its dependencies, which differ from the server's.
COPY .mastra/worker/package.json .mastra/worker/package-lock.json* ./.mastra/worker/
RUN cd .mastra/worker && npm install --force --prefer-offline --no-audit --no-fund
COPY .mastra/worker ./.mastra/worker

EXPOSE 4111
CMD ["node", "index.mjs"]
.dockerignore
.mastra/output/node_modules
.mastra/worker/node_modules

The server still starts with CMD ["node", "index.mjs"] and Studio assets stay at /app/studio, so nothing about the Server or Studio deployment changes. The worker entrypoint lands at /app/.mastra/worker/index.mjs, which is the chart's default mastra-workers.command. If you lay your image out differently, override that value to match.

Use a new tag for each build

Push each build under its own tag. The chart's default imagePullPolicy is IfNotPresent, so re-pushing an existing tag leaves nodes running the cached digest and your worker pods keep crashing on the old bundle.

Enable workers
Direct link to Enable workers

Workers run the same image as the Server — mastra build emits the worker entrypoint alongside the server entrypoint. Setting global.image once keeps every component on the same build:

mastra-values.yaml
global:
image:
repository: your-registry/my-mastra-app
tag: '1.0.0'

mastra-server:
existingSecret: mastra-app-env
# Required: stop the API from also running workers in-process.
workers:
splitDeployment: true

mastra-workers:
enabled: true
server:
# Must match mastra-server.existingSecret; the chart verifies this.
existingSecret: mastra-app-env
roles:
orchestration:
enabled: true
replicaCount: 2
scheduler:
enabled: true
backgroundTasks:
enabled: true

Worker pods mount the Server's ConfigMap and Secret rather than defining their own, so their configuration is identical to the API's by construction. You never maintain two copies of the same environment.

Roles and scaling
Direct link to Roles and scaling

RoleScalesNotes
orchestrationYesConsumes the workflows topic and calls back into the API to execute steps. The consumer group distributes work across replicas.
schedulerNo — always 1Polls storage for due cron schedules. The chart pins it to one replica and uses the Recreate strategy, because a rolling update would briefly run two schedulers and fire every schedule twice.
backgroundTasksYesExecutes agent tool calls marked background: { enabled: true } off the request path.

The two scalable roles support autoscaling. Utilization targets need matching resource requests, and the chart fails the render if you enable autoscaling without them:

mastra-values.yaml
mastra-workers:
roles:
orchestration:
resources:
requests:
cpu: 200m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 80

CPU utilization is a rough proxy for queue depth. To scale on the number of pending events, use KEDA with a Redis Streams scaler against the same consumer group.

Separate worker images
Direct link to Separate worker images

If your build produces separate API and worker images rather than one image with both entrypoints, the chart's default lockstep check will reject the mismatch. Opt out explicitly:

mastra-values.yaml
mastra-workers:
image:
tag: worker
# Server and workers intentionally run different builds; keeping them on the
# same source commit becomes your responsibility.
allowServerSkew: true
command: ['node', 'index.mjs'] # match your worker image's entrypoint

The guard exists because a version skew between the API and the workers is silent on both sides: the workers start, consume the queue, and run stale step code against the new API.

Restarting workers after a config change
Direct link to Restarting workers after a config change

Worker pods don't restart automatically when the Server's ConfigMap or Secret changes — Helm's checksum annotations only cover objects a chart owns, and those belong to mastra-server. Roll them yourself:

kubectl rollout restart deploy -n mastra -l app.kubernetes.io/name=mastra-workers,app.kubernetes.io/instance=mastra

Production checklist
Direct link to Production checklist

  • Reference credentials with existingSecret instead of plaintext values.
  • Set resource requests via mastra-server.resources and mastra-studio.resources.
  • Prefer standalone cert-manager and ingress controller installs over the bundled toggles; CRD lifecycle inside an umbrella chart complicates upgrades.
  • Enable multiple replicas only after configuring shared storage, distributed PubSub, shared cache, and the required process roles.
  • When running background workers, set mastra-server.workers.splitDeployment: true, run exactly one scheduler, and set maxmemory-policy to noeviction on Redis or Valkey.
  • The chart applies hardened defaults: non-root containers, read-only root filesystem, seccomp RuntimeDefault, and no service account token automount.

Upgrade and uninstall
Direct link to Upgrade and uninstall

Upgrade to a new chart version or roll out a new image tag with the same command:

CHART_VERSION=0.2.0
CHART=oci://us-central1-docker.pkg.dev/mastra-cloud/mastra-helm-ee/mastra-projects

helm upgrade mastra "$CHART" \
--version "$CHART_VERSION" \
-n mastra -f mastra-values.yaml

The chart hashes config and secret contents into pod annotations, so configuration changes trigger a rolling restart automatically.

Uninstall the release:

helm uninstall mastra -n mastra

Your database and object store are unaffected, because the chart never manages stateful services.

Troubleshooting
Direct link to Troubleshooting

  • helm install/upgrade fails with 401 Unauthorized or UNAUTHORIZED. Your registry access token is missing or expired. Request a fresh token from https://license.mastra.ai/v1/registry-token using your Enterprise license and run helm registry login again.
  • Pods crash with Cannot find module '@libsql/linux-x64-gnu' (or similar). The image contains node_modules built for a different platform. Install dependencies inside the image and exclude host-built node_modules via .dockerignore, then rebuild with --platform matching your nodes.
  • Pods crash-loop with a license error. Production mode requires a valid MASTRA_EE_LICENSE in your secret when enterprise features are configured.
  • Studio shows the server API instead of the UI. The image was built without Studio assets. Rebuild with mastra build --studio.
  • Worker pods crash-loop with Cannot find module '/app/.mastra/worker/index.mjs'. The image was built without the worker artifact. Run mastra worker build --output-dir .mastra/worker and copy it into the image, or override mastra-workers.command to match your layout.
  • Scheduled workflows fire twice. The API is still running workers in-process alongside the worker Deployments. Set mastra-server.workers.splitDeployment: true.
  • Workflows stall with no error. The orchestration worker can't reach the API, or Redis evicted unacknowledged events. Check mastra-workers.server.port against mastra-server.service.port, confirm any restrictive NetworkPolicy admits worker pods, and verify maxmemory-policy is noeviction.
  • Worker pods stay in CreateContainerConfigError. They mount the Server's ConfigMap and Secret, so mastra-workers.server.existingSecret must match mastra-server.existingSecret, and mastra-workers can't be installed as a standalone release.
  • Ingress has no address. Confirm an ingress controller is running, or set ingressController.install=true.
  • Certificates stay pending in gateway mode. Some Gateway controllers can't serve ACME challenges until a listener certificate exists. Check the chart's per-cloud presets and release notes for the bootstrap procedure on your platform.