> Discover all available pages from the documentation index: https://mastra.ai/llms.txt # Deploy Mastra workers Run [Mastra workers](https://mastra.ai/docs/deployment/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. > **Info:** This guide covers splitting workers into their own containers. If you only need workers to run in-process alongside the API, see [Workers](https://mastra.ai/docs/deployment/workers). No extra setup is required. ## Before you begin You'll need: - A [Mastra application](https://mastra.ai/guides/getting-started/quickstart) - [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/), or a [Kubernetes](https://kubernetes.io/docs/setup/) cluster with [`kubectl`](https://kubernetes.io/docs/tasks/tools/) - A distributed PubSub backend: [Redis](https://redis.io/) for [`RedisStreamsPubSub`](https://mastra.ai/reference/pubsub/redis-streams), or a [Google Cloud](https://cloud.google.com/) project for [`GoogleCloudPubSub`](https://mastra.ai/reference/pubsub/google-cloud-pubsub) - A shared database reachable from every container. See [supported storage backends](https://mastra.ai/reference/workers/overview) for the full list. > **Warning:** 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 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**: ```typescript 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!, }), }) ``` **Google Cloud Pub/Sub + LibSQL**: ```typescript 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](https://mastra.ai/reference/workers/overview) works. Swap the storage adapter for your preferred database. ## Deploy 1. Build your Mastra application. The output runs in every container. ```bash mastra build ``` This produces a self-contained `.mastra/output/` directory. See [Deploy a Mastra server](https://mastra.ai/docs/deployment/mastra-server) for details on the build output. 2. Create a Dockerfile that copies the pre-built output and installs production dependencies: ```dockerfile FROM node:22-alpine WORKDIR /app COPY .mastra/output/package.json .mastra/output/.npmrc* ./ RUN npm install --omit=dev COPY .mastra/output/ . EXPOSE 4111 CMD ["node", "index.mjs"] ``` 3. 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_WORKERS` value to control which worker starts. The API sets `MASTRA_WORKERS: "false"` to disable all event processing. The orchestration worker sets `MASTRA_STEP_EXECUTION_URL` to point step execution requests at the API's internal URL. See [step execution URL](#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](https://mastra.ai/docs/server/auth/workers) for details. **Docker Compose**: ```yaml services: postgres: image: postgres:16-alpine environment: POSTGRES_USER: mastra POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: mastra ports: - '5432:5432' volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ['CMD-SHELL', 'pg_isready -U mastra'] interval: 5s timeout: 3s retries: 5 redis: image: redis:7-alpine ports: - '6379:6379' healthcheck: test: ['CMD', 'redis-cli', 'ping'] interval: 5s timeout: 3s retries: 5 api: build: ./app ports: - '4111:4111' environment: DATABASE_URL: postgres://mastra:${POSTGRES_PASSWORD}@postgres:5432/mastra REDIS_URL: redis://redis:6379 MASTRA_WORKERS: 'false' MASTRA_WORKER_AUTH_TOKEN: ${MASTRA_WORKER_AUTH_TOKEN} depends_on: postgres: condition: service_healthy redis: condition: service_healthy healthcheck: test: ['CMD', 'wget', '-qO-', 'http://localhost:4111/api/agents'] interval: 5s timeout: 3s retries: 5 orchestration-worker: build: ./app environment: DATABASE_URL: postgres://mastra:${POSTGRES_PASSWORD}@postgres:5432/mastra REDIS_URL: redis://redis:6379 MASTRA_WORKERS: orchestration MASTRA_STEP_EXECUTION_URL: http://api:4111/api MASTRA_WORKER_AUTH_TOKEN: ${MASTRA_WORKER_AUTH_TOKEN} depends_on: api: condition: service_healthy scheduler-worker: build: ./app environment: DATABASE_URL: postgres://mastra:${POSTGRES_PASSWORD}@postgres:5432/mastra REDIS_URL: redis://redis:6379 MASTRA_WORKERS: scheduler MASTRA_WORKER_AUTH_TOKEN: ${MASTRA_WORKER_AUTH_TOKEN} depends_on: api: condition: service_healthy background-task-worker: build: ./app environment: DATABASE_URL: postgres://mastra:${POSTGRES_PASSWORD}@postgres:5432/mastra REDIS_URL: redis://redis:6379 MASTRA_WORKERS: backgroundTasks MASTRA_WORKER_AUTH_TOKEN: ${MASTRA_WORKER_AUTH_TOKEN} depends_on: api: condition: service_healthy volumes: pgdata: ``` Create a `.env` file next to your `docker-compose.yml`: ```bash POSTGRES_PASSWORD=your-secure-password MASTRA_WORKER_AUTH_TOKEN=your-shared-secret-token ``` > **Note:** Remember to set any other environment variables your application needs (e.g., your [model provider](https://mastra.ai/models/providers) API key). **Kubernetes**: Create a namespace and a Secret with your connection strings: ```yaml apiVersion: v1 kind: Namespace metadata: name: mastra-workers ``` ```bash kubectl apply -f k8s/namespace.yaml kubectl 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' ``` > **Note:** Add any other environment variables your application needs (e.g., your [model provider](https://mastra.ai/models/providers) API key) to the Secret or as additional `--from-literal` entries. Build and push the Docker image to a registry your cluster can pull from: ```bash docker build -t your-registry/mastra-workers:latest ./app docker push your-registry/mastra-workers:latest ``` Apply 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). ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: postgres namespace: mastra-workers spec: replicas: 1 selector: matchLabels: app: postgres template: metadata: labels: app: postgres spec: containers: - name: postgres image: postgres:16-alpine ports: - containerPort: 5432 env: - name: POSTGRES_USER value: mastra - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: mastra-secrets key: POSTGRES_PASSWORD - name: POSTGRES_DB value: mastra volumeMounts: - name: pgdata mountPath: /var/lib/postgresql/data volumes: - name: pgdata emptyDir: {} --- apiVersion: v1 kind: Service metadata: name: postgres namespace: mastra-workers spec: selector: app: postgres ports: - port: 5432 targetPort: 5432 ``` > **Caution:** The Postgres example above uses `emptyDir` for storage, which means data is lost when the pod restarts. In production, replace it with a `PersistentVolumeClaim` or use a managed database service. ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: redis namespace: mastra-workers spec: replicas: 1 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: containers: - name: redis image: redis:7-alpine args: ['--appendonly', 'yes'] ports: - containerPort: 6379 --- apiVersion: v1 kind: Service metadata: name: redis namespace: mastra-workers spec: selector: app: redis ports: - port: 6379 targetPort: 6379 ``` ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: api namespace: mastra-workers spec: replicas: 1 selector: matchLabels: app: api template: metadata: labels: app: api spec: containers: - name: api image: your-registry/mastra-workers:latest ports: - containerPort: 4111 env: - name: MASTRA_WORKERS value: 'false' envFrom: - secretRef: name: mastra-secrets readinessProbe: httpGet: path: /api/agents port: 4111 initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: httpGet: path: /api/agents port: 4111 initialDelaySeconds: 15 periodSeconds: 10 resources: requests: cpu: 500m memory: 512Mi --- apiVersion: v1 kind: Service metadata: name: api namespace: mastra-workers spec: selector: app: api ports: - port: 4111 targetPort: 4111 ``` ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: orchestration-worker namespace: mastra-workers spec: replicas: 1 selector: matchLabels: app: orchestration-worker template: metadata: labels: app: orchestration-worker spec: containers: - name: worker image: your-registry/mastra-workers:latest env: - name: MASTRA_WORKERS value: orchestration - name: MASTRA_STEP_EXECUTION_URL value: http://api:4111/api envFrom: - secretRef: name: mastra-secrets resources: requests: cpu: 250m memory: 256Mi ``` ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: scheduler-worker namespace: mastra-workers spec: replicas: 1 selector: matchLabels: app: scheduler-worker template: metadata: labels: app: scheduler-worker spec: containers: - name: worker image: your-registry/mastra-workers:latest env: - name: MASTRA_WORKERS value: scheduler envFrom: - secretRef: name: mastra-secrets resources: requests: cpu: 250m memory: 256Mi ``` ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: background-task-worker namespace: mastra-workers spec: replicas: 1 selector: matchLabels: app: background-task-worker template: metadata: labels: app: background-task-worker spec: containers: - name: worker image: your-registry/mastra-workers:latest env: - name: MASTRA_WORKERS value: backgroundTasks envFrom: - secretRef: name: mastra-secrets resources: requests: cpu: 250m memory: 256Mi ``` Apply all manifests and wait for the API to become ready: ```bash kubectl apply -f k8s/ kubectl wait -n mastra-workers --for=condition=ready pod -l app=api --timeout=90s kubectl wait -n mastra-workers --for=condition=ready pod -l app=orchestration-worker --timeout=60s kubectl wait -n mastra-workers --for=condition=ready pod -l app=scheduler-worker --timeout=60s kubectl wait -n mastra-workers --for=condition=ready pod -l app=background-task-worker --timeout=60s ``` 4. Verify the stack is running and the API responds: **Docker Compose**: ```bash docker compose up -d docker compose ps curl http://localhost:4111/api/agents ``` **Kubernetes**: ```bash kubectl get pods -n mastra-workers kubectl port-forward -n mastra-workers svc/api 4111:4111 ``` In a separate terminal: ```bash curl http://localhost:4111/api/agents ``` A JSON list of your agents confirms the API and workers are running. ## 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: ```bash 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. ## 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**: ```bash docker compose up -d --scale orchestration-worker=3 docker compose up -d --scale background-task-worker=2 ``` **Kubernetes**: ```bash 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: ```yaml 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 ``` > **Note:** CPU-based autoscaling needs the [metrics-server](https://github.com/kubernetes-sigs/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 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. > **Warning:** 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. ## Related - [Workers](https://mastra.ai/docs/deployment/workers): What workers are and when to use them - [Worker authentication](https://mastra.ai/docs/server/auth/workers): Secure worker-to-API communication - [Workers reference](https://mastra.ai/reference/workers/overview): Configuration details for all worker types - [CLI reference](https://mastra.ai/reference/cli/mastra): `mastra worker build` and `mastra worker start` - [PubSub](https://mastra.ai/docs/server/pubsub): Event delivery backends - [Deploy a Mastra server](https://mastra.ai/docs/deployment/mastra-server): Build output and server configuration - [Deploy Mastra to Kubernetes](https://mastra.ai/guides/deployment/kubernetes): Multi-pod deployment with durable agents