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.7) plus a rolling latest.

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

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>:<PASSWORD>@host:5432/resonate" \
  resonatehqio/resonate:v0.9.7

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>:<PASSWORD>@host:5432/resonate"
pool_size = 20

For MySQL, the equivalent block is:

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

[storage.mysql]
url = "mysql://<USER>:<PASSWORD>@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>:<PASSWORD>@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#

For the full client + server CLI surface (subcommands, scenarios, worked examples), see CLI reference. The table below covers the operator flags most commonly needed when running resonate serve.

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>:<PASSWORD>@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>:<PASSWORD>@localhost:5432/resonate"
pool_size = 20

Or with CLI flags:

code
resonate serve \
  --storage-type postgres \
  --storage-postgres-url "postgres://<USER>:<PASSWORD>@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>:<PASSWORD>@localhost:3306/resonate"
pool_size = 20

Or with CLI flags:

code
resonate serve \
  --storage-type mysql \
  --storage-mysql-url "mysql://<USER>:<PASSWORD>@localhost:3306/resonate" \
  --storage-mysql-pool-size 20

Or with environment variables:

code
RESONATE_STORAGE__TYPE=mysql
RESONATE_STORAGE__MYSQL__URL=mysql://<USER>:<PASSWORD>@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.7
    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.7 \
  --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.7
          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

Defaults#

The most commonly referenced server defaults:

  • HTTP API port8001 (--server-port)
  • Bind address0.0.0.0 (--server-bind); host advertised as localhost
  • Storage backendsqlite at resonate.db
  • --tasks-lease-timeout15_000 ms
  • --tasks-retry-timeout30_000 ms (lower to 500 ms for chained / recursive workflows)
  • Prometheus metrics port9090 (0 disables)
  • Log levelinfo
  • Postgres / MySQL pool size10
  • --transports-http-push-enabled / --transports-http-poll-enabledtrue
  • --transports-gcps-enabled / --transports-bash-exec-enabledfalse

These mirror the operator surface above; the canonical cross-component reference (including SDK init, ctx.run, retry policy, and env-var defaults) is the Defaults reference.

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.