Kubernetes (Helm)
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 buildand 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 beginDirect 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
ingressmode: an ingress controller, or let the chart install one - For
gatewaymode: Gateway API CRDs and a Gateway controller (for example GKE's managed Gateway)
DeployDirect link to Deploy
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.
Build your Mastra project and containerize the output. Pass
--studioso the same image can serve both Server and Studio:mastra build --studioDockerfileFROM node:22-slimWORKDIR /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-fundCOPY .mastra/output ./EXPOSE 4111CMD ["node", "index.mjs"].dockerignore.mastra/output/node_moduleswarningDon't
COPYthe host-builtnode_modulesinto the image.mastra buildinstalls native modules for your local platform — an image built on an Apple Silicon Mac for an amd64 cluster will crash at startup with errors likeCannot 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.
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 mastrakubectl 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.Create a values file pointing the chart at your image and secret:
mastra-values.yamlglobal:cloud: generic # gke | eks | aks | generic | localmastra-server:image:repository: your-registry/my-mastra-apptag: '1.0.0'existingSecret: mastra-app-envmastra-studio:image:repository: your-registry/my-mastra-apptag: '1.0.0'existingSecret: mastra-app-envBoth 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.cloudto 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.yamlmastra-server:imagePullSecrets:- name: mastra-registrymastra-studio:imagePullSecrets:- name: mastra-registryPublic 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.0CHART=oci://us-central1-docker.pkg.dev/mastra-cloud/mastra-helm-ee/mastra-projectshelm show values "$CHART" --version "$CHART_VERSION"helm show readme "$CHART" --version "$CHART_VERSION"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.0CHART=oci://us-central1-docker.pkg.dev/mastra-cloud/mastra-helm-ee/mastra-projectsTOKEN=$(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-stdinhelm install mastra "$CHART" \--version "$CHART_VERSION" \-n mastra -f mastra-values.yamlTokens 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.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=mastrakubectl -n mastra get svccurl http://<server-external-ip>:4111/healthA
{"success":true}response means the server is up. Open the Studio service address in a browser to reach the Mastra Studio UI.
Exposure modesDirect link to Exposure modes
The chart supports three ways to expose Server and Studio, selected once via global.exposure.mode:
| Mode | What you get | Required values |
|---|---|---|
loadBalancer (default) | One LoadBalancer Service per component with per-cloud annotations | — |
ingress | ClusterIP Services plus one Ingress per component | mastra-server.ingress.host, mastra-studio.ingress.host |
gateway | ClusterIP Services, a shared Gateway, and one HTTPRoute per component | mastra-server.httpRoute.host, mastra-studio.httpRoute.host |
Ingress with Let's Encrypt TLSDirect 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:
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 APIDirect 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:
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 presetsDirect link to Cloud presets
The chart ships a values preset per platform that sets global.cloud and platform-appropriate defaults:
| Platform | Preset | Notes |
|---|---|---|
| GKE | values-gke.yaml | Gateway API recommended; Workload Identity supported via serviceAccount.annotations |
| EKS | values-eks.yaml | NLB by default; ALB via ingress annotations; IRSA supported |
| AKS | values-aks.yaml | Azure LoadBalancer annotations |
| Local (kind/minikube) | values-local.yaml | Ingress on api.localhost / studio.localhost |
Object storageDirect 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-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 observabilityDirect 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 replicasDirect 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
RedisStreamsPubSubandRedisServerCache, withREDIS_URLstored 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-server:
resources:
requests:
cpu: 250m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 80
Background workersDirect 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.
RequirementsDirect 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
schedulesdomain, 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.
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 imageDirect 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:
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"]
.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.
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 workersDirect 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:
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 scalingDirect link to Roles and scaling
| Role | Scales | Notes |
|---|---|---|
orchestration | Yes | Consumes the workflows topic and calls back into the API to execute steps. The consumer group distributes work across replicas. |
scheduler | No — always 1 | Polls 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. |
backgroundTasks | Yes | Executes 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-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 imagesDirect 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-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 changeDirect 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 checklistDirect link to Production checklist
- Reference credentials with
existingSecretinstead of plaintext values. - Set resource requests via
mastra-server.resourcesandmastra-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 setmaxmemory-policytonoevictionon 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 uninstallDirect 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.
TroubleshootingDirect link to Troubleshooting
helm install/upgradefails with401 UnauthorizedorUNAUTHORIZED. Your registry access token is missing or expired. Request a fresh token fromhttps://license.mastra.ai/v1/registry-tokenusing your Enterprise license and runhelm registry loginagain.- Pods crash with
Cannot find module '@libsql/linux-x64-gnu'(or similar). The image containsnode_modulesbuilt for a different platform. Install dependencies inside the image and exclude host-builtnode_modulesvia.dockerignore, then rebuild with--platformmatching your nodes. - Pods crash-loop with a license error. Production mode requires a valid
MASTRA_EE_LICENSEin 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. Runmastra worker build --output-dir .mastra/workerand copy it into the image, or overridemastra-workers.commandto 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.portagainstmastra-server.service.port, confirm any restrictive NetworkPolicy admits worker pods, and verifymaxmemory-policyisnoeviction. - Worker pods stay in
CreateContainerConfigError. They mount the Server's ConfigMap and Secret, somastra-workers.server.existingSecretmust matchmastra-server.existingSecret, andmastra-workerscan'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.
RelatedDirect link to Related
- Kubernetes: Hand-written manifests and multi-pod scaling with shared pub/sub
- Mastra server: Build output and server behavior
- Deployment overview
- Workers: Split background processing into separate containers
- Worker authentication: Secure worker-to-API communication
- Scheduled workflows: Declare cron schedules on workflows