# Resonate Server Deployment

> Deploy and configure the Resonate server on Linux systems with systemd. Covers installation, public URL configuration, JWT authentication, and troubleshooting.

- Skill: `resonatehq/resonate-server-deployment` (Agent Skill)
- Install (CLI): `npx skillmds@latest add resonatehq/resonate-server-deployment`
- Raw SKILL.md: https://api.skillmd.com/api/skills/resonatehq/resonate-server-deployment/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: Apache-2.0
- Author: resonatehq (https://skillmd.com/u/resonatehq)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/resonatehq/resonate-server-deployment

---


# Resonate Server Deployment

## Overview

Deploy the Resonate server on Linux systems using systemd for process management. This skill covers installation, configuration of public URLs, JWT authentication setup, and common troubleshooting scenarios.

Deploying on GCP instead? See [`resonate-server-deployment-cloud-run`](../resonate-server-deployment-cloud-run/SKILL.md) for the Cloud Run + Cloud SQL variant.

## Prerequisites

- Linux system with systemd
- Root/sudo access
- curl installed
- (Optional) openssl for generating JWT keys
- (Optional) jwt-cli for generating tokens

## Quick Start

### Basic Deployment (No Auth)

```bash
# Download and run
curl -L -o deploy-resonate.sh https://example.com/deploy-resonate.sh
chmod +x deploy-resonate.sh
sudo ./deploy-resonate.sh
```

### Deployment with Public URL and JWT Auth

```bash
# Generate RSA key pair first
openssl genrsa -out private_key.pem 2048
openssl rsa -in private_key.pem -pubout -out public_key.pem

# Deploy with configuration (setting the public key enables JWT auth)
sudo RESONATE_SERVER__URL=https://resonate.example.com \
     RESONATE_AUTH__PUBLICKEY=/path/to/public_key.pem \
     ./deploy-resonate.sh
```

## Configuration Options

| Environment Variable | Default | Description |
|---------------------|---------|-------------|
| `RESONATE_VERSION` | v0.9.8 | Server version the install script downloads |
| `RESONATE_SERVER__PORT` | 8001 | HTTP API port |
| `RESONATE_SERVER__URL` | (none) | Public URL for the server (e.g., `https://resonate.example.com`) |
| `RESONATE_AUTH__PUBLICKEY` | (none) | Path to JWT public key file; **setting it enables JWT auth** (there is no separate enable flag) |

## Server Flags Reference

The Resonate server binary accepts these flags:

```bash
resonate serve [flags]

Flags:
  --server-url string           Public URL for the server (included in response headers)
  --server-port int             HTTP API port (default 8001)
  --auth-publickey string       Path to JWT public key for authentication
  --storage-type string         Storage backend: sqlite, postgres, or mysql (default sqlite)
  --storage-postgres-url string PostgreSQL connection URL
  --storage-mysql-url string    MySQL connection URL
  --storage-mysql-pool-size int MySQL connection pool size (default 10)
```

All flags are also settable via `RESONATE_`-prefixed environment variables with `__` for nesting, e.g. `RESONATE_SERVER__URL`, `RESONATE_AUTH__PUBLICKEY`, `RESONATE_STORAGE__POSTGRES__URL`.

## Architecture

```
                    Internet
                       │
                       ▼
              ┌────────────────┐
              │  Nginx/Caddy   │  (SSL termination)
              │  Port 443      │
              └────────────────┘
                       │
                       ▼
              ┌────────────────┐
              │ Resonate Server│  (systemd service)
              │ Port 8001      │
              └────────────────┘
                       │
              ┌────────┴────────┐
              ▼                 ▼
       ┌──────────┐      ┌──────────┐
       │ Worker 1 │      │ Worker 2 │
       └──────────┘      └──────────┘
```

## Systemd Service Configuration

### Basic Service File

```ini
# /etc/systemd/system/resonate.service
[Unit]
Description=Resonate Server
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/var/lib/resonate
ExecStart=/usr/local/bin/resonate serve
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=resonate
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
```

### Service File with Public URL and Auth

```ini
# /etc/systemd/system/resonate.service
[Unit]
Description=Resonate Server
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/var/lib/resonate
ExecStart=/usr/local/bin/resonate serve \
  --server-url https://resonate.example.com \
  --auth-publickey /etc/resonate/public_key.pem
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=resonate
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
```

## JWT Authentication Setup

### 1. Generate RSA Key Pair

```bash
# Generate private key (keep secret!)
openssl genrsa -out private_key.pem 2048

# Extract public key (deploy with server)
openssl rsa -in private_key.pem -pubout -out public_key.pem
```

### 2. Install JWT CLI (for generating tokens)

```bash
# macOS
brew install mike-engel/jwt-cli/jwt-cli

# Linux (download binary)
curl -L -o jwt https://github.com/mike-engel/jwt-cli/releases/latest/download/jwt-linux
chmod +x jwt
sudo mv jwt /usr/local/bin/
```

### 3. Generate Client Tokens

**IMPORTANT:** An empty payload `{}` will DENY all access. You must include a `prefix` claim.

```bash
# Unrestricted access (empty prefix = all promises)
jwt encode --secret @private_key.pem -A RS256 '{"prefix":""}'

# Admin access (alternative way to get full access)
jwt encode --secret @private_key.pem -A RS256 '{"role":"admin"}'

# Restricted to prefix (only access promises starting with "my-app")
jwt encode --secret @private_key.pem -A RS256 '{"prefix":"my-app"}'

# Unrestricted with expiration
jwt encode --secret @private_key.pem -A RS256 --exp='+30 days' '{"prefix":""}'
```

**Prefix claim behavior:**
| Payload | Access |
|---------|--------|
| `{}` | DENIED (no prefix claim = unauthorized) |
| `{"prefix": ""}` | ALL promises (empty string = unrestricted) |
| `{"prefix": "my-app"}` | Only promises starting with `my-app` |
| `{"role": "admin"}` | ALL promises (admin role) |

### 4. Configure Server

```bash
# Copy public key to server
scp public_key.pem root@server:/etc/resonate/

# Update service (public key present → JWT auth on)
sudo RESONATE_AUTH__PUBLICKEY=/etc/resonate/public_key.pem \
     ./deploy-resonate.sh --update-service
```

### 5. Configure Clients

Pass the server URL and a JWT token to the client. **TypeScript shown** — every SDK takes a URL + token the same way; see the per-SDK skill (`resonate-basic-ephemeral-world-usage-{typescript,python,rust,go}`) for client init in your language. The `RESONATE_TOKEN` env var and the `Authorization: Bearer <jwt>` header are identical across all SDKs.

```typescript
// SDK client
const resonate = new Resonate({
  url: "https://resonate.example.com",
  token: process.env.RESONATE_TOKEN  // JWT token
});

// Direct HTTP calls
const headers = {
  "Content-Type": "application/json",
  "Authorization": `Bearer ${token}`
};
```

## Why API URL Matters

The `--server-url` flag tells the Resonate server its public address. This is critical when:

1. **Workers poll for tasks**: The server returns URLs that workers use for callbacks
2. **Clients connect from different networks**: The server advertises its address
3. **Behind a reverse proxy**: Server needs to know the external URL, not localhost

**Without `--server-url`:**
- Server returns `http://localhost:8001` in responses
- External workers can't reach callback URLs
- Polling may fail with connection errors

**With `--server-url`:**
- Server returns `https://resonate.example.com` in responses
- Workers and clients use the correct public URL

## Directory Structure

```
/usr/local/bin/
└── resonate              # Server binary

/etc/resonate/
├── public_key.pem        # JWT public key (if auth enabled)
└── resonate.yml          # Optional config file

/var/lib/resonate/
└── resonate.db           # SQLite database (default)

/etc/systemd/system/
└── resonate.service      # Systemd service file
```

## Common Operations

### View Logs

```bash
# Follow logs
journalctl -u resonate -f

# Last 100 lines
journalctl -u resonate -n 100

# Logs since boot
journalctl -u resonate -b

# Logs from specific time
journalctl -u resonate --since "2024-01-28 10:00:00"
```

### Service Management

```bash
# Status
systemctl status resonate

# Start/Stop/Restart
systemctl start resonate
systemctl stop resonate
systemctl restart resonate

# Enable/Disable on boot
systemctl enable resonate
systemctl disable resonate

# Reload service file after changes
systemctl daemon-reload
```

### Check Configuration

```bash
# View current service configuration
systemctl cat resonate

# Check what command is running
ps aux | grep resonate

# Test server is responding
curl http://localhost:8001/promises
```

## Troubleshooting

### 401 Unauthorized Errors

**Symptoms:**
- Clients get 401 errors
- "missing authorization header" in logs
- Workers can't poll for tasks

**Causes & Fixes:**

1. **Auth enabled on server but client has no token** — add a token to the client (TypeScript shown; every SDK takes a token the same way — see the per-SDK skills)
   ```typescript
   // Add token to client
   const resonate = new Resonate({
     url: "https://resonate.example.com",
     token: process.env.RESONATE_TOKEN
   });
   ```

2. **Token is expired**
   ```bash
   # Generate new token (a payload with no prefix/role claim is DENIED — include one)
   jwt encode --secret @private_key.pem -A RS256 --exp='+30 days' '{"prefix":""}'
   ```

3. **Wrong public key on server**
   ```bash
   # Verify key matches
   openssl rsa -in private_key.pem -pubout | diff - public_key.pem
   ```

4. **Token signed with wrong private key**
   ```bash
   # Decode and verify token
   jwt decode $TOKEN
   ```

### Server Not Responding

**Symptoms:**
- `curl http://localhost:8001` hangs or refuses connection
- Service shows as active but port not open

**Causes & Fixes:**

1. **Service crashed on startup**
   ```bash
   journalctl -u resonate -n 50
   # Look for error messages
   ```

2. **Port already in use**
   ```bash
   netstat -tlnp | grep 8001
   # Kill conflicting process or change port
   ```

3. **Firewall blocking port**
   ```bash
   # Check UFW (Ubuntu)
   ufw status
   # Allow internal access only (recommended)
   # Don't expose 8001 directly - use reverse proxy
   ```

### Workers Can't Connect

**Symptoms:**
- Workers start but never receive tasks
- "connection refused" errors

**Causes & Fixes:**

1. **Wrong RESONATE_URL in worker**
   ```bash
   # Should be public URL if server is remote
   RESONATE_URL=https://resonate.example.com
   # NOT http://localhost:8001 (unless worker is on same machine)
   ```

2. **Missing --server-url on server**
   ```bash
   # Server doesn't know its public URL
   # Update service with --server-url flag
   ```

3. **SSL/TLS issues**
   ```bash
   # Test with curl
   curl -v https://resonate.example.com/promises
   # Check certificate is valid
   ```

### Database Issues

**Symptoms:**
- "database is locked" errors
- Data not persisting across restarts

**Causes & Fixes:**

1. **Multiple processes accessing SQLite**
   ```bash
   # Only one server should access the DB
   ps aux | grep resonate
   # Kill duplicates
   ```

2. **Wrong working directory**
   ```bash
   # Check where DB is being created
   find / -name "resonate.db" 2>/dev/null
   # Ensure WorkingDirectory is set in service file
   ```

3. **Disk full**
   ```bash
   df -h
   # Clean up or expand disk
   ```

## Production Checklist

- [ ] Server binary installed and versioned
- [ ] Systemd service file created and enabled
- [ ] `--server-url` set to public URL
- [ ] JWT authentication enabled (if needed)
- [ ] Public key deployed to `/etc/resonate/`
- [ ] Reverse proxy configured (Nginx/Caddy)
- [ ] SSL certificate installed
- [ ] Firewall configured (only 443 exposed)
- [ ] Logs rotating (journald handles this)
- [ ] Monitoring configured (port 9090 metrics)
- [ ] Backup strategy for database

## Reverse Proxy Configuration

### Nginx

```nginx
server {
    listen 443 ssl;
    server_name resonate.example.com;

    ssl_certificate /etc/letsencrypt/live/resonate.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/resonate.example.com/privkey.pem;

    location / {
        proxy_pass http://localhost:8001;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # For long-polling
        proxy_read_timeout 120s;
        proxy_send_timeout 120s;
    }
}
```

### Caddy

```caddyfile
resonate.example.com {
    reverse_proxy localhost:8001
}
```

## Summary

Key deployment steps:
1. Install binary from GitHub releases
2. Create systemd service with appropriate flags
3. Configure `--server-url` for public accessibility
4. Enable JWT auth with `--auth-publickey` if needed
5. Set up reverse proxy with SSL
6. Configure clients with correct URL and token

**Critical flags:**
- `--server-url`: Server's public URL (required for external access)
- `--auth-publickey`: JWT public key path (required for auth)

