Skip to main content

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.

info

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 begin
Direct link to Before you begin

You'll need:

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

src/mastra/index.ts
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!,
}),
})

Any supported storage backend works. Swap the storage adapter for your preferred database.

Deploy
Direct link to Deploy

  1. Build your Mastra application. The output runs in every container.

    mastra build

    This produces a self-contained .mastra/output/ directory. See Deploy a Mastra server for details on the build output.

  2. Create a Dockerfile that copies the pre-built output and installs production dependencies:

    app/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. Create a docker-compose.yml that runs the fully split topology. The file defines six services: a database, a PubSub backend, the API server, and three workers. Each worker container runs the same image with a different MASTRA_WORKERS value to control which worker starts.

    The API container 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 for details.

    docker-compose.yml
    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'
    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
    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
    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
    depends_on:
    api:
    condition: service_healthy

    volumes:
    pgdata:

    Create a .env file next to your docker-compose.yml:

    .env
    POSTGRES_PASSWORD=your-secure-password
    note

    Remember to set any other environment variables your application needs (e.g., your model provider API key).

  4. Start the stack and verify the API responds:

    docker compose up -d
    docker compose ps
    curl http://localhost:4111/api/agents

Step execution URL
Direct 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.

Scaling
Direct 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 up -d --scale orchestration-worker=3
docker compose up -d --scale background-task-worker=2

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