Run a server

Start the Resonate server with persistent storage.

This page will help you install and run the Resonate Server.

First install the Server on your machine.

Install with Homebrew#

You can install the latest Resonate Server version with Homebrew via:

code
brew install resonatehq/tap/resonate

All available releases and associated release artifacts are available on Github.

Once installed, you can start the server with an in-memory store by running:

Run using an in-memory store
resonate dev

Or, you can run the server with the default SQLite store by running:

Run using the default SQLite store
resonate serve

You will see log output like the following:

code
INFO resonate: Resonate Server starting port=8001
INFO resonate: Using SQLite backend path="resonate.db"
INFO resonate: Auth disabled — all requests accepted

The HTTP API is on port 8001 and the Prometheus metrics endpoint is on port 9090. Both default ports can be changed.

Run with Docker#

The Resonate Server is published as an official image on Docker Hub at resonatehqio/resonate. Tags track the server release versions (e.g. v0.9.5) plus a rolling latest.

Run the Resonate Server in Docker
docker run --rm -p 8001:8001 -p 9090:9090 resonatehqio/resonate:v0.9.5

resonate serve is the default command. Override only when you need to change behavior:

Use Postgres storage
docker run --rm -p 8001:8001 -p 9090:9090 \
  -e RESONATE_STORAGE__TYPE=postgres \
  -e RESONATE_STORAGE__POSTGRES__URL="postgres://user:pass@host:5432/resonate" \
  resonatehqio/resonate:v0.9.5

The image exposes both 8001 (HTTP API) and 9090 (Prometheus metrics).

Configuration#

The server is configured by layering, in this order — each layer overrides the previous:

  1. Built-in defaults
  2. resonate.toml (optional, looked up in the working directory)
  3. Environment variables prefixed with RESONATE_ using __ to nest
  4. CLI flags passed to resonate serve

The recommended approach for any non-trivial deployment is a resonate.toml file checked into your infra repo, with secrets injected via environment variables.

resonate.toml#

resonate.toml
level = "info"

[server]
host = "localhost"
port = 8001

[storage]
type = "sqlite"

[storage.sqlite]
path = "resonate.db"

[tasks]
lease_timeout = 15000
retry_timeout = 30000

For Postgres, swap the [storage] section:

resonate.toml — Postgres
[storage]
type = "postgres"

[storage.postgres]
url = "postgres://user:pass@host:5432/resonate"
pool_size = 20

For MySQL, the equivalent block is:

resonate.toml — MySQL
[storage]
type = "mysql"

[storage.mysql]
url = "mysql://user:pass@host:3306/resonate"
pool_size = 20

To enable JWT auth, add an [auth] section:

resonate.toml — auth enabled
[auth]
publickey = "/etc/resonate/auth/public.pem"

Environment variables#

Every config field is also reachable as an environment variable. The convention is RESONATE_<SECTION>__<FIELD>, with __ separating nested keys:

code
RESONATE_LEVEL=info
RESONATE_SERVER__PORT=8001
RESONATE_STORAGE__TYPE=postgres
RESONATE_STORAGE__POSTGRES__URL=postgres://user:pass@host:5432/resonate
RESONATE_AUTH__PUBLICKEY=/etc/resonate/auth/public.pem

This is the form used in the official Docker Compose file and is the right choice for container platforms that prefer env-var injection.

CLI flags#

Most settings can also be passed as CLI flags — useful for ad-hoc overrides:

code
resonate serve \
  --storage-type postgres \
  --storage-postgres-url "postgres://user:pass@localhost:5432/resonate" \
  --storage-postgres-pool-size 20

Run resonate serve --help for the full list. Common flags:

FlagDefaultDescription
--server-hostlocalhostHTTP server host
--server-port8001HTTP server port
--server-bind0.0.0.0Bind address
--server-urlhttp://{host}:{port}Externally-reachable URL (required for distributed deployments where workers call back to the server)
--levelinfoLog level: debug, info, warn, error
--storage-typesqliteStorage backend: sqlite, postgres, or mysql
--storage-postgres-urlPostgres connection string
--storage-postgres-pool-size10Postgres connection pool size
--storage-mysql-urlMySQL connection string
--storage-mysql-pool-size10MySQL connection pool size
--auth-publickeyPath to JWT public key (PEM format) for authentication
--server-cors-allow-originAllowed CORS origin (repeatable; use * for permissive). See Security › CORS
--tasks-lease-timeout15000 (ms)Task lease timeout
--tasks-retry-timeout30000 (ms)Suspend/wake retry interval. Lower values (e.g. 500) significantly reduce latency for chained or recursive workflows
--observability-metrics-port9090Prometheus metrics port (0 to disable)
tasks-retry-timeout

The default --tasks-retry-timeout of 30 seconds adds significant latency to chained workflows (sagas, recursive fan-out). For most deployments, set this to 500 (milliseconds) to keep suspend/wake cycles fast.

Transport flags#

v0.9.5 added explicit enable/disable flags for every transport so operators can shape which delivery paths are live per deployment. See Message transports for the per-transport behaviour.

FlagDefaultDescription
--transports-http-push-enabledtrueEnable the http:// / https:// push transport
--transports-http-poll-enabledtrueEnable the SSE poll transport used by long-polling SDK clients
--transports-gcps-enabledfalseEnable the gcps:// GCP Pub/Sub transport
--transports-gcps-projectDefault GCP project ID for the Pub/Sub transport
--transports-bash-exec-enabledfalseEnable the bash:/// local-script transport (see Bash exec)

Outbound HTTP push authentication flags#

v0.9.5 added outbound authentication for HTTP push deliveries so workers behind authenticated endpoints (for example GCP Cloud Run with --no-allow-unauthenticated) can verify the server's identity. See Outbound HTTP push authentication for the full configuration guide.

FlagDefaultDescription
--transports-http-push-auth-modenoneOutbound auth mode: none, bearer, or gcp
--transports-http-push-auth-tokenStatic bearer token (used only when mode = bearer)
--transports-http-push-auth-auddelivery target URLFixed OIDC audience (used only when mode = gcp; defaults to the per-delivery target URL)
--transports-http-push-auth-headerAuthorizationName of the request header that carries the token

Run with PostgreSQL#

For production deployments, use PostgreSQL as the storage backend.

With a resonate.toml (recommended):

resonate.toml
[storage]
type = "postgres"

[storage.postgres]
url = "postgres://user:pass@localhost:5432/resonate"
pool_size = 20

Or with CLI flags:

code
resonate serve \
  --storage-type postgres \
  --storage-postgres-url "postgres://user:pass@localhost:5432/resonate" \
  --storage-postgres-pool-size 20

Migrations are applied automatically on first startup.

See the Availability guide for high-availability PostgreSQL configuration.

Run with MySQL#

MySQL is a third storage option alongside SQLite and PostgreSQL. Use it when your operational tooling already centers on MySQL.

With a resonate.toml (recommended):

resonate.toml
[storage]
type = "mysql"

[storage.mysql]
url = "mysql://user:pass@localhost:3306/resonate"
pool_size = 20

Or with CLI flags:

code
resonate serve \
  --storage-type mysql \
  --storage-mysql-url "mysql://user:pass@localhost:3306/resonate" \
  --storage-mysql-pool-size 20

Or with environment variables:

code
RESONATE_STORAGE__TYPE=mysql
RESONATE_STORAGE__MYSQL__URL=mysql://user:pass@localhost:3306/resonate
RESONATE_STORAGE__MYSQL__POOL_SIZE=20

Migrations are applied automatically on first startup. MySQL 8.0 or later is required (the schema relies on generated columns and JSON path expressions).

A minimal Docker Compose stack that boots the server against a MySQL service:

docker-compose.yml — MySQL
services:
  resonate-server:
    image: resonatehqio/resonate:v0.9.5
    ports:
      - "8001:8001"
      - "9090:9090"
    environment:
      RESONATE_SERVER__BIND: "0.0.0.0"
      RESONATE_STORAGE__TYPE: mysql
      RESONATE_STORAGE__MYSQL__URL: mysql://resonate:resonate@mysql:3306/resonate
      RESONATE_STORAGE__MYSQL__POOL_SIZE: 20
    depends_on:
      mysql:
        condition: service_healthy

  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: resonate
      MYSQL_USER: resonate
      MYSQL_PASSWORD: resonate
      MYSQL_DATABASE: resonate
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "resonate", "-presonate"]
      interval: 5s
      timeout: 5s
      retries: 10
VARCHAR(255) limit on identifiers

The MySQL schema stores promise IDs, task IDs, listener addresses, and a few derived tag values (resonate:target, resonate:origin, resonate:branch) as VARCHAR(255). Inputs that exceed 255 characters fail with a 400 InvalidInput response. SQLite and Postgres do not enforce this bound, so applications written against those backends should verify ID lengths before switching to MySQL.

Deploy to GCP Cloud Run#

Cloud Run can pull the official image directly:

code
gcloud run deploy resonate-server \
  --image=resonatehqio/resonate:v0.9.5 \
  --region=<your-region> \
  --port=8001 \
  --allow-unauthenticated \
  --max-instances=1

Set the --server-url flag in the container args (or RESONATE_SERVER__URL env var) to your service URL after the first deploy so workers can call back correctly. For Postgres, set RESONATE_STORAGE__TYPE=postgres and RESONATE_STORAGE__POSTGRES__URL.

For a worked end-to-end example, see Cloud Run workers.

Run on Kubernetes#

The recommended Kubernetes deployment is a single-replica Deployment that pulls the official image:

code
---
apiVersion: v1
kind: Service
metadata:
  name: resonate
spec:
  selector:
    app: resonate
  ports:
    - port: 8001
      name: api
    - port: 9090
      name: metrics
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: resonate
spec:
  replicas: 1
  selector:
    matchLabels:
      app: resonate
  template:
    metadata:
      labels:
        app: resonate
    spec:
      containers:
        - name: resonate
          image: resonatehqio/resonate:v0.9.5
          env:
            - name: RESONATE_SERVER__BIND
              value: "0.0.0.0"
            - name: RESONATE_STORAGE__TYPE
              value: "postgres"
            - name: RESONATE_STORAGE__POSTGRES__URL
              valueFrom:
                secretKeyRef:
                  name: resonate-secrets
                  key: database-url
            - name: RESONATE_TASKS__RETRY_TIMEOUT
              value: "500"
          ports:
            - name: api
              containerPort: 8001
            - name: metrics
              containerPort: 9090
          livenessProbe:
            httpGet:
              path: /health
              port: 8001
            initialDelaySeconds: 5
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 8001
            initialDelaySeconds: 5
            periodSeconds: 5

Create a file named resonate-k8s.yaml and apply the manifest:

code
kubectl apply -f resonate-k8s.yaml

To remove Resonate from your cluster:

code
kubectl delete -f resonate-k8s.yaml

Health and readiness#

The server exposes two HTTP endpoints for liveness and readiness checks:

EndpointReturnsUse for
GET /health200 OK always (as long as the process is up)Liveness probes
GET /ready200 OK if storage is reachable, 503 otherwiseReadiness probes
Quick health check
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8001/health
# 200

Build from source#

If you don't have Homebrew, you can build the server from source. The server is written in Rust:

code
git clone https://github.com/resonatehq/resonate
cd resonate
cargo build --release
./target/release/resonate serve

Ports#

The server binds two ports by default:

PortPurpose
:8001HTTP API (all SDK traffic, SSE polling, and task dispatch)
:9090Prometheus metrics
PaaS port routing

On platforms like Railway, Render, or Fly.io that auto-detect a single primary port, explicitly route to 8001. Without this, the platform may pick the metrics port (9090) and route all traffic there, resulting in 502 errors.