Deploy Mastra workers
Run Mastra workers as separate processes so you can scale orchestration, scheduling, and background tasks independently from the API. This guide walks through a fully split deployment using Docker Compose or Kubernetes.
This guide covers splitting workers into their own containers. If you only need workers to run in-process alongside the API, see Workers. No extra setup is required.
Before you beginDirect link to Before you begin
You'll need:
- A Mastra application
- Docker and Docker Compose, or a Kubernetes cluster with
kubectl - A distributed PubSub backend: Redis for
RedisStreamsPubSub, or a Google Cloud project forGoogleCloudPubSub - A shared database reachable from every container. See supported storage backends for the full list.
The default in-memory PubSub can't deliver events across processes. You must configure a distributed PubSub backend before splitting workers into separate containers.
Configure shared infrastructureDirect link to Configure shared infrastructure
Point the Mastra instance at a distributed PubSub backend and a shared database. Use environment variables so the same image runs in every container.
- Redis Streams + PostgreSQL
- Google Cloud Pub/Sub + LibSQL
import { Mastra } from '@mastra/core/mastra'
import { RedisStreamsPubSub } from '@mastra/redis-streams'
import { PostgresStore } from '@mastra/pg'
export const mastra = new Mastra({
storage: new PostgresStore({
connectionString: process.env.DATABASE_URL!,
}),
pubsub: new RedisStreamsPubSub({
url: process.env.REDIS_URL!,
}),
})
import { Mastra } from '@mastra/core/mastra'
import { GoogleCloudPubSub } from '@mastra/google-cloud-pubsub'
import { LibSQLStore } from '@mastra/libsql'
export const mastra = new Mastra({
storage: new LibSQLStore({
url: process.env.DATABASE_URL!,
}),
pubsub: new GoogleCloudPubSub({
projectId: process.env.GCP_PROJECT_ID!,
}),
})
Any supported storage backend works. Swap the storage adapter for your preferred database.
DeployDirect link to Deploy
Build your Mastra application. The output runs in every container.
mastra buildThis produces a self-contained
.mastra/output/directory. See Deploy a Mastra server for details on the build output.Create a Dockerfile that copies the pre-built output and installs production dependencies:
app/DockerfileFROM node:22-alpineWORKDIR /appCOPY .mastra/output/package.json .mastra/output/.npmrc* ./RUN npm install --omit=devCOPY .mastra/output/ .EXPOSE 4111CMD ["node", "index.mjs"]Define the fully split topology. The setup runs six services: a database, a PubSub backend, the API server, and three workers. Each worker runs the same image with a different
MASTRA_WORKERSvalue to control which worker starts.The API sets
MASTRA_WORKERS: "false"to disable all event processing. The orchestration worker setsMASTRA_STEP_EXECUTION_URLto point step execution requests at the API's internal URL. See step execution URL for details.All services share a
MASTRA_WORKER_AUTH_TOKEN. Workers include this token in requests to the API so the API can verify the caller is a trusted internal service. See worker authentication for details.- Docker Compose
- Kubernetes
docker-compose.ymlservices:postgres:image: postgres:16-alpineenvironment:POSTGRES_USER: mastraPOSTGRES_PASSWORD: ${POSTGRES_PASSWORD}POSTGRES_DB: mastraports:- '5432:5432'volumes:- pgdata:/var/lib/postgresql/datahealthcheck:test: ['CMD-SHELL', 'pg_isready -U mastra']interval: 5stimeout: 3sretries: 5redis:image: redis:7-alpineports:- '6379:6379'healthcheck:test: ['CMD', 'redis-cli', 'ping']interval: 5stimeout: 3sretries: 5api:build: ./appports:- '4111:4111'environment:DATABASE_URL: postgres://mastra:${POSTGRES_PASSWORD}@postgres:5432/mastraREDIS_URL: redis://redis:6379MASTRA_WORKERS: 'false'MASTRA_WORKER_AUTH_TOKEN: ${MASTRA_WORKER_AUTH_TOKEN}depends_on:postgres:condition: service_healthyredis:condition: service_healthyhealthcheck:test: ['CMD', 'wget', '-qO-', 'http://localhost:4111/api/agents']interval: 5stimeout: 3sretries: 5orchestration-worker:build: ./appenvironment:DATABASE_URL: postgres://mastra:${POSTGRES_PASSWORD}@postgres:5432/mastraREDIS_URL: redis://redis:6379MASTRA_WORKERS: orchestrationMASTRA_STEP_EXECUTION_URL: http://api:4111/apiMASTRA_WORKER_AUTH_TOKEN: ${MASTRA_WORKER_AUTH_TOKEN}depends_on:api:condition: service_healthyscheduler-worker:build: ./appenvironment:DATABASE_URL: postgres://mastra:${POSTGRES_PASSWORD}@postgres:5432/mastraREDIS_URL: redis://redis:6379MASTRA_WORKERS: schedulerMASTRA_WORKER_AUTH_TOKEN: ${MASTRA_WORKER_AUTH_TOKEN}depends_on:api:condition: service_healthybackground-task-worker:build: ./appenvironment:DATABASE_URL: postgres://mastra:${POSTGRES_PASSWORD}@postgres:5432/mastraREDIS_URL: redis://redis:6379MASTRA_WORKERS: backgroundTasksMASTRA_WORKER_AUTH_TOKEN: ${MASTRA_WORKER_AUTH_TOKEN}depends_on:api:condition: service_healthyvolumes:pgdata:Create a
.envfile next to yourdocker-compose.yml:.envPOSTGRES_PASSWORD=your-secure-passwordMASTRA_WORKER_AUTH_TOKEN=your-shared-secret-tokennoteRemember to set any other environment variables your application needs (e.g., your model provider API key).
Create a namespace and a Secret with your connection strings:
k8s/namespace.yamlapiVersion: v1kind: Namespacemetadata:name: mastra-workerskubectl apply -f k8s/namespace.yamlkubectl create secret generic mastra-secrets -n mastra-workers \--from-literal=POSTGRES_PASSWORD='your-password' \--from-literal=DATABASE_URL='postgresql://mastra:your-password@postgres:5432/mastra' \--from-literal=REDIS_URL='redis://redis:6379' \--from-literal=MASTRA_WORKER_AUTH_TOKEN='your-shared-token'noteAdd any other environment variables your application needs (e.g., your model provider API key) to the Secret or as additional
--from-literalentries.Build and push the Docker image to a registry your cluster can pull from:
docker build -t your-registry/mastra-workers:latest ./appdocker push your-registry/mastra-workers:latestApply Deployments and Services for the database, PubSub backend, API, and three workers. The example below uses in-cluster Postgres and Redis. In production, use managed services (e.g., Amazon RDS, Cloud SQL, ElastiCache, Memorystore).
k8s/postgres.yamlapiVersion: apps/v1kind: Deploymentmetadata:name: postgresnamespace: mastra-workersspec:replicas: 1selector:matchLabels:app: postgrestemplate:metadata:labels:app: postgresspec:containers:- name: postgresimage: postgres:16-alpineports:- containerPort: 5432env:- name: POSTGRES_USERvalue: mastra- name: POSTGRES_PASSWORDvalueFrom:secretKeyRef:name: mastra-secretskey: POSTGRES_PASSWORD- name: POSTGRES_DBvalue: mastravolumeMounts:- name: pgdatamountPath: /var/lib/postgresql/datavolumes:- name: pgdataemptyDir: {}---apiVersion: v1kind: Servicemetadata:name: postgresnamespace: mastra-workersspec:selector:app: postgresports:- port: 5432targetPort: 5432cautionThe Postgres example above uses
emptyDirfor storage, which means data is lost when the pod restarts. In production, replace it with aPersistentVolumeClaimor use a managed database service.k8s/redis.yamlapiVersion: apps/v1kind: Deploymentmetadata:name: redisnamespace: mastra-workersspec:replicas: 1selector:matchLabels:app: redistemplate:metadata:labels:app: redisspec:containers:- name: redisimage: redis:7-alpineargs: ['--appendonly', 'yes']ports:- containerPort: 6379---apiVersion: v1kind: Servicemetadata:name: redisnamespace: mastra-workersspec:selector:app: redisports:- port: 6379targetPort: 6379k8s/api.yamlapiVersion: apps/v1kind: Deploymentmetadata:name: apinamespace: mastra-workersspec:replicas: 1selector:matchLabels:app: apitemplate:metadata:labels:app: apispec:containers:- name: apiimage: your-registry/mastra-workers:latestports:- containerPort: 4111env:- name: MASTRA_WORKERSvalue: 'false'envFrom:- secretRef:name: mastra-secretsreadinessProbe:httpGet:path: /api/agentsport: 4111initialDelaySeconds: 10periodSeconds: 5livenessProbe:httpGet:path: /api/agentsport: 4111initialDelaySeconds: 15periodSeconds: 10resources:requests:cpu: 500mmemory: 512Mi---apiVersion: v1kind: Servicemetadata:name: apinamespace: mastra-workersspec:selector:app: apiports:- port: 4111targetPort: 4111k8s/orchestration-worker.yamlapiVersion: apps/v1kind: Deploymentmetadata:name: orchestration-workernamespace: mastra-workersspec:replicas: 1selector:matchLabels:app: orchestration-workertemplate:metadata:labels:app: orchestration-workerspec:containers:- name: workerimage: your-registry/mastra-workers:latestenv:- name: MASTRA_WORKERSvalue: orchestration- name: MASTRA_STEP_EXECUTION_URLvalue: http://api:4111/apienvFrom:- secretRef:name: mastra-secretsresources:requests:cpu: 250mmemory: 256Mik8s/scheduler-worker.yamlapiVersion: apps/v1kind: Deploymentmetadata:name: scheduler-workernamespace: mastra-workersspec:replicas: 1selector:matchLabels:app: scheduler-workertemplate:metadata:labels:app: scheduler-workerspec:containers:- name: workerimage: your-registry/mastra-workers:latestenv:- name: MASTRA_WORKERSvalue: schedulerenvFrom:- secretRef:name: mastra-secretsresources:requests:cpu: 250mmemory: 256Mik8s/background-task-worker.yamlapiVersion: apps/v1kind: Deploymentmetadata:name: background-task-workernamespace: mastra-workersspec:replicas: 1selector:matchLabels:app: background-task-workertemplate:metadata:labels:app: background-task-workerspec:containers:- name: workerimage: your-registry/mastra-workers:latestenv:- name: MASTRA_WORKERSvalue: backgroundTasksenvFrom:- secretRef:name: mastra-secretsresources:requests:cpu: 250mmemory: 256MiApply all manifests and wait for the API to become ready:
kubectl apply -f k8s/kubectl wait -n mastra-workers --for=condition=ready pod -l app=api --timeout=90skubectl wait -n mastra-workers --for=condition=ready pod -l app=orchestration-worker --timeout=60skubectl wait -n mastra-workers --for=condition=ready pod -l app=scheduler-worker --timeout=60skubectl wait -n mastra-workers --for=condition=ready pod -l app=background-task-worker --timeout=60sVerify the stack is running and the API responds:
- Docker Compose
- Kubernetes
docker compose up -ddocker compose pscurl http://localhost:4111/api/agentskubectl get pods -n mastra-workerskubectl port-forward -n mastra-workers svc/api 4111:4111In a separate terminal:
curl http://localhost:4111/api/agentsA JSON list of your agents confirms the API and workers are running.
Step execution URLDirect link to Step execution URL
In a fully split deployment, the orchestration worker runs in a separate container from the API. When it processes a workflow event, it delegates step execution to the API over HTTP.
Set MASTRA_STEP_EXECUTION_URL to the API's internal URL, including the /api prefix:
MASTRA_STEP_EXECUTION_URL=http://api:4111/api
The orchestration worker sends a POST request to ${MASTRA_STEP_EXECUTION_URL}/workflows/:workflowId/runs/:runId/steps/execute for each step. The API resolves the workflow and executes the step locally.
Without this variable, the orchestration worker attempts to execute steps in-process. That works when the worker runs alongside the API, but fails in split deployments where the worker doesn't have access to the full Mastra runtime.
ScalingDirect link to Scaling
The orchestration and background task workers are safe to scale horizontally. PubSub consumer groups distribute events across instances, so each event is processed once:
- Docker Compose
- Kubernetes
docker compose up -d --scale orchestration-worker=3
docker compose up -d --scale background-task-worker=2
kubectl scale deployment/orchestration-worker -n mastra-workers --replicas=3
kubectl scale deployment/background-task-worker -n mastra-workers --replicas=2
For automatic scaling, add a HorizontalPodAutoscaler:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: orchestration-worker
namespace: mastra-workers
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: orchestration-worker
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
CPU-based autoscaling needs the metrics-server running in the cluster. Managed clusters like GKE, EKS, and AKS include it by default.
The API can also scale horizontally behind a load balancer.
Don't scale the scheduler worker. Run exactly one instance. Multiple schedulers polling the same storage fire duplicate events for the same schedule.
Crash recoveryDirect link to Crash recovery
Workers recover from crashes because the distributed PubSub backend persists unacknowledged events:
- Orchestration worker: Pending events stay in the PubSub backend. When the worker restarts, it picks up where it left off.
- Scheduler worker: No events are missed permanently. The scheduler computes the next fire time from the current time on restart, not from where it left off.
- API during step execution: The orchestration worker's HTTP request fails. The event is nacked and redelivered on the next attempt.
If the API crashes while a step is already executing (e.g., mid-sleep), that step's work is lost. The workflow run may remain stuck in a running state. Mastra doesn't yet have automatic timeout-based recovery for this scenario.
RelatedDirect link to Related
- Workers: What workers are and when to use them
- Worker authentication: Secure worker-to-API communication
- Workers reference: Configuration details for all worker types
- CLI reference:
mastra worker buildandmastra worker start - PubSub: Event delivery backends
- Deploy a Mastra server: Build output and server configuration
- Deploy Mastra to Kubernetes: Multi-pod deployment with durable agents