> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sevalla.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> These are the docs for Sevalla, a platform for deploying applications, databases, static sites, object storage and load balancers. The dashboard is at https://app.sevalla.com.
> Bold text in steps is an exact dashboard label.
> To act on a Sevalla account, prefer the Sevalla MCP server (https://mcp.sevalla.com/mcp, see https://docs.sevalla.com/ai-agents/overview.md), then the Sevalla CLI (https://docs.sevalla.com/developer-tools/cli.md), then the REST API (https://api-docs.sevalla.com, OpenAPI spec at https://api.sevalla.com/v3/openapi.json).
> Every page index is at https://docs.sevalla.com/llms.txt.

# Deploy n8n

> Run n8n from its official Docker image with PostgreSQL, the right webhook URL and a fixed encryption key, then scale it with queue mode, Valkey and background workers.

n8n runs on Sevalla as an application from its official Docker image, `n8nio/n8n`: the web process serves the editor, the REST API and your workflows' webhooks, and a Sevalla PostgreSQL database keeps workflows, credentials, executions and files, so the application needs no disk. In queue mode, the web process puts each execution on a Valkey queue and background workers run them, so long or busy workflows don't slow down the editor or the webhooks.

## At a glance

| | |
| - | - |
| Source | The `n8nio/n8n:2.45.6` Docker image |
| Start command | The image's own (`tini -- /docker-entrypoint.sh`, which starts `n8n`); `exec n8n worker --concurrency=10` for workers |
| Port | `8080`, from `N8N_PORT` (n8n's own default is `5678`) |
| Processes | Web process; in queue mode, a `Worker` background worker |
| Services | PostgreSQL 18 over an internal connection; in queue mode, Valkey 9.0 over an internal connection |
| Disk | None |
| Health check | `GET /healthz/readiness` returns `200` once n8n has connected to the database and is ready (readiness probe) |
| Size | **S1** for the web process and each worker. **H1** is too small: n8n is stopped with exit code `137` (out of memory) while it starts |

### Environment variables

| Variable | Required | Available during | Secret | Value |
| - | - | - | - | - |
| `DB_TYPE` | Yes | Runtime | No | `postgresdb` |
| `DB_POSTGRESDB_HOST` | Yes | Runtime | No | `DB_HOST` from the internal connection, renamed |
| `DB_POSTGRESDB_PORT` | Yes | Runtime | No | `DB_PORT` from the internal connection, renamed |
| `DB_POSTGRESDB_DATABASE` | Yes | Runtime | No | `DB_DATABASE` from the internal connection, renamed |
| `DB_POSTGRESDB_USER` | Yes | Runtime | No | `DB_USERNAME` from the internal connection, renamed |
| `DB_POSTGRESDB_PASSWORD` | Yes | Runtime | Yes | `DB_PASSWORD` from the internal connection, renamed |
| `N8N_ENCRYPTION_KEY` | Yes | Runtime | Yes | Generate with `openssl rand -base64 32`, and keep a copy: see [Keep the encryption key](#keep-the-encryption-key) |
| `N8N_PORT` | Yes | Runtime | No | `8080`, the web process port |
| `N8N_WEBHOOK_URL` | Yes | Runtime | No | The application's URL with a trailing slash, for example `https://n8n-x7k2m.sevalla.app/` |
| `N8N_PROXY_HOPS` | Yes | Runtime | No | `2` in Oracle Cloud Infrastructure (OCI) locations, `3` in Google Cloud locations |
| `N8N_DEFAULT_BINARY_DATA_MODE` | Yes | Runtime | No | `database` |
| `N8N_GRACEFUL_SHUTDOWN_TIMEOUT` | Yes | Runtime | No | `20` |
| `EXECUTIONS_MODE` | Queue mode | Runtime | No | `queue` |
| `QUEUE_BULL_REDIS_HOST` | Queue mode | Runtime | No | `REDIS_HOST` from the Valkey internal connection, renamed |
| `QUEUE_BULL_REDIS_PORT` | Queue mode | Runtime | No | `REDIS_PORT` from the Valkey internal connection, renamed |
| `QUEUE_BULL_REDIS_PASSWORD` | Queue mode | Runtime | Yes | `REDIS_PASSWORD` from the Valkey internal connection, renamed |
| `OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS` | Queue mode | Runtime | No | `true` |
| `N8N_WORKER_SUSPEND_EXECUTIONS_ON_SHUTDOWN` | Queue mode | Runtime | No | `true` |
| `GENERIC_TIMEZONE` | No | Runtime | No | The time zone of your schedules, for example `Europe/Berlin`. n8n's default is `America/New_York` |

An application from a Docker image has no build, so every variable is a runtime variable. [Set the environment variables](#set-the-environment-variables) explains each one.

## Before you begin

* A Sevalla company with a payment method: n8n needs the **S1** size or larger.
* OpenSSL on your computer, to generate the encryption key (`openssl version` prints its version).

## Create the database

**1. Open the create form**

In the sidebar, click **Databases**, then click **Create database**.

**2. Choose PostgreSQL**

Click **PostgreSQL** and choose the newest version. This guide was tested with PostgreSQL 18.

**3. Name and place it**

Enter the **Display name** `n8n-db`, choose the **Data center** where n8n will run, and choose the **Size**. This guide was tested on **DB1**.

**4. Create it**

Click **Create database** and wait for the status **Ready**.

## Create the application

**1. Choose the image**

In the sidebar, click **Applications**, then click **Create** > **Application**. Under **Source**, click **Docker image** and enter `n8nio/n8n:2.45.6` in **Docker image**. Keep the **Registry credential** at **None (public image)**.

**2. Name, place and size it**

Enter the **Name** `n8n`, choose the database's **Data center**, and choose the **Size** **S1**.

**3. Create it without deploying**

Click **Create application**. The application opens on its **Overview** tab with the status **Not deployed yet**. Note its domain under the name, for example `n8n-x7k2m.sevalla.app`: `N8N_WEBHOOK_URL` needs it.

`n8nio/n8n` is the image n8n publishes on Docker Hub; n8n's own examples also use `docker.n8n.io/n8nio/n8n`, which serves the same image. The tag pins one n8n release: `2.45.6` is the current stable release. Moving tags such as `latest` and `stable` change under you on the next deployment, and an n8n upgrade migrates the database, so pin a version and [upgrade on purpose](#upgrade-n8n).

## Connect the database

n8n reads its database settings from five separate variables, not a connection URL. The internal connection adds the same values under other names; rename them as you add them.

**1. Open the database's networking**

In the sidebar, click **Databases**, then click `n8n-db`. Click the **Networking** tab.

**2. Add the connection**

Under **Private networking**, in **Connected applications**, click **Add internal connection**, and choose the `n8n` **Application**.

**3. Rename the variables**

Turn on **Add environment variables to the application**. Change the keys `DB_HOST` to `DB_POSTGRESDB_HOST`, `DB_PORT` to `DB_POSTGRESDB_PORT`, `DB_DATABASE` to `DB_POSTGRESDB_DATABASE`, `DB_USERNAME` to `DB_POSTGRESDB_USER` and `DB_PASSWORD` to `DB_POSTGRESDB_PASSWORD`. Click the trash icon next to `DB_URL`: n8n doesn't read it.

**4. Save**

Click **Add internal connection**.

## Set the environment variables

Generate the encryption key on your computer, and store it in your password manager before you add it:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
openssl rand -base64 32
```

On the application's **Environment variables** tab, click **Add environment variables**, paste these lines into the first key field, replace the key and the domain with yours, and click **Add environment variables**:

```bash .env theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DB_TYPE=postgresdb
N8N_ENCRYPTION_KEY=Q2hhbmdlIG1lOiBnZW5lcmF0ZSB5b3VyIG93biBrZXk=
N8N_PORT=8080
N8N_WEBHOOK_URL=https://n8n-x7k2m.sevalla.app/
N8N_PROXY_HOPS=2
N8N_DEFAULT_BINARY_DATA_MODE=database
N8N_GRACEFUL_SHUTDOWN_TIMEOUT=20
```

| Variable | Why n8n needs it on Sevalla |
| - | - |
| `DB_TYPE` | Stores everything in PostgreSQL instead of a SQLite file in the container, which every deployment would wipe. |
| `N8N_ENCRYPTION_KEY` | Encrypts the credentials you save in n8n. Without it, n8n generates a key into `/home/node/.n8n/config`, a file that the next deployment deletes, and workers refuse to start without the variable. |
| `N8N_PORT` | n8n listens on `5678` unless told otherwise, and doesn't read `PORT`. `8080` matches the web process port, so you don't change the port on the **Networking** tab. |
| `N8N_WEBHOOK_URL` | The public URL n8n builds webhook and editor links from. Without it, the editor shows webhook URLs such as `http://localhost:8080/webhook/orders`, and the startup log says `Editor is now accessible via: http://localhost:8080`. It replaces the older `WEBHOOK_URL`, which n8n 2.45 still reads but reports as deprecated. |
| `N8N_PROXY_HOPS` | How many proxies add an entry to `X-Forwarded-For` before a request reaches n8n: Cloudflare and Sevalla's ingress in OCI locations, one more proxy in Google Cloud locations ([which locations use which cloud](https://docs.sevalla.com/platform/locations.md)). n8n then rate-limits sign-in attempts by the visitor's address. Without it, the log shows `ValidationError: The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default).` on the first sign-in. |
| `N8N_DEFAULT_BINARY_DATA_MODE` | Keeps files that flow through workflows (attachments, downloads) in PostgreSQL. n8n's default in regular mode writes them to `/home/node/.n8n/storage`, which the next deployment deletes. Queue mode uses `database` by default. |
| `N8N_GRACEFUL_SHUTDOWN_TIMEOUT` | Sevalla stops an instance 30 seconds after the shutdown begins and sends `SIGTERM` after 5 of them. n8n's default of 30 seconds would be cut off; 20 lets it finish its shutdown first. See [Running executions during a deployment](#running-executions-during-a-deployment). |

You don't need `N8N_HOST` or `N8N_PROTOCOL`: n8n uses them only to build the URL when `N8N_WEBHOOK_URL` is empty. Keep `N8N_PROTOCOL` at its default `http`: Cloudflare and Sevalla handle HTTPS, and n8n serves plain HTTP on `8080`.

## Add a readiness probe

n8n takes about 10 seconds to start, and runs its database migrations first. A readiness probe keeps sending requests to the running version until the new one is ready.

**1. Open the web process**

Click the **Processes** tab, click **⋯** on the web process, then click **Update process**.

**2. Turn on the probe**

Under **Health checks**, turn on **Readiness probe** and enter the **Path** `/healthz/readiness`. Keep the **Port** at `8080`.

**3. Save**

Click **Update process**.

In the tests, a deployment with the probe answered every request; without it, requests got `503` for about 8 seconds while the new instance started. The probe also protects you from a broken start: a deployment whose instance never becomes ready fails after 20 minutes with `❌ Failed to deploy Web.` in its build log, and the previous version keeps running. See [Keep the encryption key](#keep-the-encryption-key) for one cause.

## Deploy n8n

Click the **Overview** tab, click **Deploy now**, then click **Deploy**. The deployment pulls the image and starts the web process; there's no build.

> [!WARNING]
>
> Until you create the owner account, anyone who opens the application's domain can create it and take over the instance. Create it right after the first deployment.

**1. Open n8n**

When the deployment is **Deployed**, click the domain under the application's name.

**2. Create the owner account**

n8n shows **Set up owner account**. Enter your email address, name and a password of at least 8 characters with a number and a capital letter, then click **Next**.

## Verify the deployment

1. On the **Deployments** tab, the deployment's status is **Deployed**. With the API, [Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) returns `"status": "success"`.

2. The application's [runtime logs](https://docs.sevalla.com/applications/logs.md) show `Version: 2.45.6`, then these two lines once n8n is ready. Search them for `Editor is now accessible via:`, or call [Get application runtime logs](https://api-docs.sevalla.com/v3/applications/get-application-runtime-logs) (`getApplicationRuntimeLogsV3`) with `search=Editor`.

   ```text Runtime logs theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   Editor is now accessible via:
   https://n8n-x7k2m.sevalla.app
   ```

3. The readiness endpoint answers:

   ```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   curl -s https://n8n-x7k2m.sevalla.app/healthz/readiness
   ```

   ```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   {"status":"ok"}
   ```

## Run a workflow from a webhook

This workflow answers a `POST` to `/webhook/orders` with a message built by a Code node.

```json Order received theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{
  "name": "Order received",
  "nodes": [
    {
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.2,
      "position": [0, 0],
      "parameters": { "httpMethod": "POST", "path": "orders", "responseMode": "lastNode", "options": {} }
    },
    {
      "name": "Reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [240, 0],
      "parameters": {
        "jsCode": "const order = $input.first().json.body;\nreturn [{ json: { message: `Order ${order.id} received`, items: order.items.length } }];"
      }
    }
  ],
  "connections": {
    "Webhook": { "main": [[{ "node": "Reply", "type": "main", "index": 0 }]] }
  },
  "settings": { "executionOrder": "v1" }
}
```

**1. Create a workflow**

On n8n's **Overview** page, click **Build a workflow**. Once you have workflows, the button is **Create workflow**.

**2. Paste the workflow**

Copy the JSON, click the canvas, and press Ctrl+V (Cmd+V on macOS). When n8n asks **Pasting this from an AI tool?**, click **Skip**. The **Webhook** and **Reply** nodes appear and the workflow is named **Order received**.

**3. Publish it**

Click **Publish** at the top right, keep the **Version name**, and click **Publish**. Production webhooks answer only for published workflows.

Double-click the **Webhook** node and click **Production URL**: the editor shows `https://n8n-x7k2m.sevalla.app/webhook/orders`, built from `N8N_WEBHOOK_URL`. Call it:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST https://n8n-x7k2m.sevalla.app/webhook/orders \
  -H "Content-Type: application/json" \
  -d '{"id":"A-1001","items":["mug","tea"]}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"message":"Order A-1001 received","items":2}
```

The workflow's **Executions** tab lists the run as **Succeeded**. Before you publish the workflow, the same request returns `404` with `The requested webhook "POST orders" is not registered.`

## Keep the encryption key

`N8N_ENCRYPTION_KEY` must stay the same for the life of the instance. n8n 2.45 stores keys derived from it in the database, and checks them at every start.

> [!WARNING]
>
> If you change or delete `N8N_ENCRYPTION_KEY`, n8n stops at startup with `Deployment key 'signing.hmac' cannot be read with this instance encryption key` and restarts in a loop. Without a readiness probe, the domain answers `no healthy upstream` until you put the old key back. Keep a copy of the key outside Sevalla, so deleting the variable or the application doesn't lose it.

With the [readiness probe](#add-a-readiness-probe), a deployment with the wrong key fails after 20 minutes and the running version keeps serving. Either way, set the variable back to the original key and deploy again: the credentials and workflows are intact.

## What needs a disk

Nothing, once PostgreSQL, `N8N_ENCRYPTION_KEY` and `N8N_DEFAULT_BINARY_DATA_MODE=database` are set. n8n's folder in the image, `/home/node/.n8n`, holds only what those settings replace or what n8n can rebuild:

| In `/home/node/.n8n` | Without a disk |
| - | - |
| `config`, with a generated encryption key | Not used: the key comes from `N8N_ENCRYPTION_KEY` |
| `storage/`, files from executions | Not used: files go to PostgreSQL with `N8N_DEFAULT_BINARY_DATA_MODE=database` |
| Event logs and the crash journal | Recreated at each start. They help n8n recover after a crash, not after a deployment |
| `nodes/`, community nodes installed from the editor | Lost at the next deployment. Install community nodes only if you can install them again |

In the tests, workflows, credentials, executions and the owner account survived every deployment. A process with a disk is limited to 1 instance and stops during each deployment ([Persistent storage](https://docs.sevalla.com/applications/disks.md)), so a disk would only cost you availability.

## Scale with queue mode

In queue mode, the web process keeps the editor, the API and the webhooks, and puts each execution in a queue in Valkey; background workers take executions from the queue and run them. Add workers when executions slow down the editor or wait for each other.

### Create and connect Valkey

**1. Create the database**

Click **Databases**, then **Create database**. Click **Valkey**, choose the newest version (tested: 9.0), enter the **Display name** `n8n-queue`, choose the same **Data center**, and click **Create database**. Wait for **Ready**.

**2. Connect n8n**

On `n8n-queue`'s **Networking** tab, click **Add internal connection** and choose the `n8n` **Application**. Turn on **Add environment variables to the application**, change the keys `REDIS_HOST` to `QUEUE_BULL_REDIS_HOST`, `REDIS_PORT` to `QUEUE_BULL_REDIS_PORT` and `REDIS_PASSWORD` to `QUEUE_BULL_REDIS_PASSWORD`, delete `REDIS_URL`, and click **Add internal connection**.

**3. Add the queue variables**

On the `n8n` application's **Environment variables** tab, add these variables:

```bash .env theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
EXECUTIONS_MODE=queue
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true
N8N_WORKER_SUSPEND_EXECUTIONS_ON_SHUTDOWN=true
```

`OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS` sends the runs you start in the editor to the workers too; without it, n8n logs a deprecation warning in queue mode. `N8N_WORKER_SUSPEND_EXECUTIONS_ON_SHUTDOWN` keeps executions alive across deployments: see [Running executions during a deployment](#running-executions-during-a-deployment).

### Add a worker

**1. Create the process**

Click the **Processes** tab, click **Create process**, then pick **Background worker**.

**2. Set the command and size**

Enter the **Name** `Worker` and the **Custom start command** `exec n8n worker --concurrency=10`, and pick the **Resource** **S1**.

**3. Create and deploy**

Click **Create process**, then deploy the application.

The worker uses the same image and the same environment variables as the web process. A custom start command runs in a shell that doesn't pass `SIGTERM` on, so `exec` replaces the shell with n8n, which then receives `SIGTERM` and shuts down cleanly. `--concurrency=10` is how many executions one worker runs at a time; with `N8N_CONCURRENCY_PRODUCTION_LIMIT` set, that variable wins. To run more executions in parallel, raise the worker's **Instance count**: each instance takes executions from the same queue.

Keep the web process at 1 instance. Several main instances at once are n8n's multi-main setup, which needs an n8n license that includes it; without one, n8n refuses to start in that mode.

### Verify queue mode

1. Search the runtime logs for `n8n worker is now ready`. The worker logs:

   ```text Runtime logs theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   n8n worker is now ready
    * Version: 2.45.6
    * Concurrency: 10
    * Pool: (default)
    * Queue: jobs
   ```

2. Call the [webhook workflow](#run-a-workflow-from-a-webhook) again. It answers the same way, and the runtime logs show the web process handing the execution to the worker:

   ```text Runtime logs theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   Enqueued execution 14 (job 5)
   Worker started execution 14 (job 5)
   Worker finished execution 14 (job 5)
   Execution 14 (job 5) finished
   ```

The execution appears on the workflow's **Executions** tab as before.

### Running executions during a deployment

Each deployment replaces the worker's instance. Sevalla sends `SIGTERM` 5 seconds after the shutdown begins and stops the instance 30 seconds after it began. n8n pauses the queue, waits for running executions, and exits after `N8N_GRACEFUL_SHUTDOWN_TIMEOUT`.

| Setting | What happened to a 60-second execution during a deployment, in the tests |
| - | - |
| Default | The worker logged `Shutdown timed out after 20 seconds` and exited. The execution stayed **Running** in the editor and never finished: n8n checks for such executions only every 180 minutes, and then marks them as crashed |
| `N8N_WORKER_SUSPEND_EXECUTIONS_ON_SHUTDOWN=true` | The worker logged `Requested suspension of 1 execution(s) at the next node boundary`, saved the execution after the step it was on, and exited. The web process queued it again 37 seconds later, the new worker continued from that step, and the execution finished as **Succeeded** |

Suspension works for production executions (webhooks, triggers, retries), not for manual runs, chat triggers or executions that stream their response, and it waits for the current node to finish, so keep each node shorter than the 20 seconds n8n has.

## Build an AI agent workflow

An AI Agent node can answer requests that reach a webhook, with a chat model and tools. A public webhook that calls a model spends money for anyone who finds it, so this one requires a token in the `Authorization` header.

**1. Add the Anthropic credential**

On n8n's **Overview** page, click the **Credentials** tab, then **Create credential**. Choose **Anthropic**, paste your Anthropic API key, and save the credential. n8n encrypts it with `N8N_ENCRYPTION_KEY` and stores it in PostgreSQL; the key isn't in any workflow or environment variable.

**2. Paste the workflow**

Create a workflow and paste this JSON, the same way as [the webhook workflow](#run-a-workflow-from-a-webhook).

**3. Choose the credentials**

Open the **Webhook** node and create a Header Auth credential for it, with the **Name** `Authorization` and the **Value** `Bearer ` followed by a long random token, for example from `openssl rand -hex 24`. Open the **Anthropic Chat Model** node and choose your Anthropic credential.

**4. Publish it**

Click **Publish**, then **Publish**.

```json Support agent theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{
  "name": "Support agent",
  "nodes": [
    {
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.2,
      "position": [0, 0],
      "parameters": { "httpMethod": "POST", "path": "support", "authentication": "headerAuth", "responseMode": "lastNode", "options": {} }
    },
    {
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [240, 0],
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.body.question }}",
        "options": { "systemMessage": "You answer questions about orders for a small shop. Use the calculator for any arithmetic. Answer in one or two sentences." }
      }
    },
    {
      "name": "Anthropic Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.6,
      "position": [160, 220],
      "parameters": {
        "model": { "__rl": true, "mode": "id", "value": "claude-haiku-5-5" },
        "options": { "maxTokensToSample": 400 }
      }
    },
    {
      "name": "Calculator",
      "type": "@n8n/n8n-nodes-langchain.toolCalculator",
      "typeVersion": 1,
      "position": [360, 220],
      "parameters": {}
    }
  ],
  "connections": {
    "Webhook": { "main": [[{ "node": "AI Agent", "type": "main", "index": 0 }]] },
    "Anthropic Chat Model": { "ai_languageModel": [[{ "node": "AI Agent", "type": "ai_languageModel", "index": 0 }]] },
    "Calculator": { "ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]] }
  },
  "settings": { "executionOrder": "v1" }
}
```

`claude-haiku-5-5` is Anthropic's fastest and cheapest current model; change the model in the **Anthropic Chat Model** node. Call the webhook with your token:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST https://n8n-x7k2m.sevalla.app/webhook/support \
  -H "Authorization: Bearer $SUPPORT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"question":"An order has 3 mugs at $12.50 each and shipping of $4.99. What is the total?"}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"output":"The total for the order is $42.49: 3 mugs at $12.50 each ($37.50) plus $4.99 shipping."}
```

The model writes the answer in its own words, so yours can differ. In the test, the agent called the model twice and the **Calculator** once, used 1,071 input and 103 output tokens, and answered in about 3 seconds; in queue mode, a worker ran it. Without the token, the webhook answers `403` with `Authorization data is wrong!`.

Keep `"settings": { "executionOrder": "v1" }` when you create workflows through n8n's API: without it, n8n runs the workflow in its legacy execution order, the agent gets an empty result from every tool, and the webhook answers `{"code":0,"message":"No item to return was found"}`. Workflows you create in the editor use `v1`.

Content an agent reads, such as a customer's message, can contain instructions meant to mislead it. Give an agent read-only tools, and let the workflow's own nodes, after a check, take actions you can't undo, such as refunds or emails. Set a spending limit in your model provider's console.

To use models from other providers in the AI Agent node, see [OpenRouter in n8n](https://docs.sevalla.com/guides/ai/openrouter.md#n8n).

## Upgrade n8n

n8n migrates its database when a new version starts, and the migration can't be undone by starting the old image again.

1. Read the release notes of every version between yours and the new one on [n8n's GitHub releases](https://github.com/n8n-io/n8n/releases).
2. Back up `n8n-db`: on its **Backups** tab, create a manual backup. See [Backups](https://docs.sevalla.com/databases/backups.md).
3. On the application's **Settings** tab, in the **Source** section, click **Update source**, change the tag in **Docker image** (for example to `n8nio/n8n:2.46.2` once it's stable), and click **Update source**.
4. Deploy. The web process runs the migrations at start; workers use the same image, so they move to the new version with the same deployment.

To go back, restore the backup and set the previous tag.

## Add a custom domain

To serve n8n from your own domain, such as `automation.example.com`, add it on the application's **Domains** tab and create the DNS records Sevalla shows; the web process must be larger than **H1**, which **S1** is. Then change `N8N_WEBHOOK_URL` to `https://automation.example.com/` and deploy. Webhook URLs you've given to other services change with it. See [Custom domains](https://docs.sevalla.com/domains/overview.md).

## Troubleshoot

| Symptom | Cause | Fix |
| - | - | - |
| The domain answers `no healthy upstream`, and the process's exit code is `137` | The web process is on **H1**: n8n runs out of memory while it starts | Use **S1** or larger |
| The log shows `Deployment key 'signing.hmac' cannot be read with this instance encryption key` | `N8N_ENCRYPTION_KEY` changed since the first start | Set it back to the original key and deploy. See [Keep the encryption key](#keep-the-encryption-key) |
| The editor shows webhook URLs starting with `http://localhost:8080/` | `N8N_WEBHOOK_URL` isn't set | Set it to the application's URL with a trailing slash and deploy |
| The log shows `ValidationError: The 'X-Forwarded-For' header is set but the Express 'trust proxy' setting is false (default).` | `N8N_PROXY_HOPS` isn't set | Set it to `2` (OCI) or `3` (Google Cloud) and deploy |
| A webhook call returns `404` with `The requested webhook "POST orders" is not registered.` | The workflow isn't published, or the path or method differs | Publish the workflow and use the **Production URL** the **Webhook** node shows |
| A webhook call returns `{"code":0,"message":"No item to return was found"}`, and the agent says a tool returned nothing | The workflow runs in the legacy execution order | Set `"executionOrder": "v1"` in the workflow's settings |
| An execution stays **Running** after a deployment | The worker stopped before the execution finished | Set `N8N_WORKER_SUSPEND_EXECUTIONS_ON_SHUTDOWN=true`. See [Running executions during a deployment](#running-executions-during-a-deployment) |

## Deploy with the API

These calls create the same deployment without the dashboard: the database, the application, the variables, the readiness probe, and the first deployment, then queue mode. They need:

* An [API key](https://docs.sevalla.com/developer-tools/api-keys.md) that can create applications and databases, in `SEVALLA_API_KEY`.
* `curl`, `jq` and `openssl`.

The application comes from a public Docker image, so no Git provider needs to be connected.

**1. Choose the location**

[List clusters](https://api-docs.sevalla.com/v3/resources/list-clusters) (`listClustersV3`) returns the locations your company can use. Pick one by its region ID, here Frankfurt:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
API=https://api.sevalla.com/v3
AUTH="Authorization: Bearer $SEVALLA_API_KEY"
CLUSTER_ID=$(curl -s "$API/resources/clusters" -H "$AUTH" | jq -r '.[] | select(.location == "eu-frankfurt-1") | .id')
```

CLI: `sevalla resources clusters`. MCP: `get_platform_options`.

**2. Look up the sizes**

[List process resource types](https://api-docs.sevalla.com/v3/resources/list-process-resource-types) (`listProcessResourceTypesV3`) and [List database resource types](https://api-docs.sevalla.com/v3/resources/list-database-resource-types) (`listDatabaseResourceTypesV3`) return the size IDs:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
S1=$(curl -s "$API/resources/process-resource-types" -H "$AUTH" | jq -r '.[] | select(.name == "s1") | .id')
DB1=$(curl -s "$API/resources/database-resource-types" -H "$AUTH" | jq -r '.[] | select(.name == "db1") | .id')
```

CLI: `sevalla resources process-types` and `sevalla resources db-types`. MCP: `get_platform_options`.

**3. Create the database**

[Create database](https://api-docs.sevalla.com/v3/databases/create-database) (`createDatabaseV3`). The API doesn't return the password later, so generate it here and keep it in a variable:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DB_PASSWORD=$(openssl rand -hex 16)
DB_ID=$(curl -s -X POST "$API/databases" -H "$AUTH" -H "Content-Type: application/json" \
  -d "$(jq -n --arg c "$CLUSTER_ID" --arg r "$DB1" --arg p "$DB_PASSWORD" \
    '{display_name:"n8n-db", type:"postgresql", version:"18", cluster_id:$c, resource_type_id:$r, db_name:"n8n", db_user:"n8n", db_password:$p}')" \
  | jq -r .id)
```

CLI: `sevalla databases create --name n8n-db --type postgresql --db-version 18 --cluster $CLUSTER_ID --resource-type $DB1 --db-name n8n --db-user n8n --db-password $DB_PASSWORD`. MCP: `sevalla_api_create` with `createDatabaseV3` and the same body (`create_database` generates a password it doesn't return, and n8n needs it).

**4. Wait for the database**

[Get database](https://api-docs.sevalla.com/v3/databases/get-database) (`getDatabaseV3`) until `status` is `ready`, then read the internal hostname:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for i in $(seq 1 60); do [ "$(curl -s "$API/databases/$DB_ID" -H "$AUTH" | jq -r .status)" = ready ] && break; sleep 10; done
DB_HOST=$(curl -s "$API/databases/$DB_ID" -H "$AUTH" | jq -r .internal_hostname)
```

CLI: `sevalla databases get $DB_ID`. MCP: `get_database`.

**5. Create the application**

[Create application](https://api-docs.sevalla.com/v3/applications/create-application) (`createApplicationV3`) with the Docker image source. It doesn't start a deployment:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
APP_ID=$(curl -s -X POST "$API/applications" -H "$AUTH" -H "Content-Type: application/json" \
  -d "$(jq -n --arg c "$CLUSTER_ID" '{display_name:"n8n", cluster_id:$c, source:"dockerImage", docker_image:"n8nio/n8n:2.45.6"}')" \
  | jq -r .id)
```

CLI: `sevalla apps create --name n8n --cluster $CLUSTER_ID --source dockerImage --docker-image n8nio/n8n:2.45.6`. MCP: `create_application` with `deploy: false`.

**6. Read the domain and the web process**

[List application domains](https://api-docs.sevalla.com/v3/applications/list-application-domains) (`listApplicationDomainsV3`) and [List processes](https://api-docs.sevalla.com/v3/applications/list-processes) (`listProcessesV3`):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for i in $(seq 1 30); do APP_HOST=$(curl -s "$API/applications/$APP_ID/domains" -H "$AUTH" | jq -r '.data[] | select(.is_primary) | .name'); [ -n "$APP_HOST" ] && break; sleep 2; done
WEB_ID=$(curl -s "$API/applications/$APP_ID/processes" -H "$AUTH" | jq -r '.data[] | select(.type == "web") | .id')
```

CLI: `sevalla apps domains list --app-id $APP_ID` and `sevalla apps processes list --app-id $APP_ID`. MCP: `get_application`.

**7. Size the web process and add the readiness probe**

[Update process](https://api-docs.sevalla.com/v3/applications/update-process) (`updateProcessV3`). The API creates the web process at **S1** already; send the size anyway if you change it. Always send `timeoutSeconds`: without it, the probe gets 120 seconds instead of the dashboard's 1:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH "$API/applications/$APP_ID/processes/$WEB_ID" -H "$AUTH" -H "Content-Type: application/json" \
  -d "$(jq -n --arg r "$S1" '{resource_type_id:$r, readiness_probe:{httpGet:{path:"/healthz/readiness", port:8080}, initialDelaySeconds:5, periodSeconds:5, timeoutSeconds:1, successThreshold:1, failureThreshold:3}}')" \
  | jq -c .readiness_probe
```

CLI: `sevalla apps processes update $WEB_ID --app-id $APP_ID --resource-type $S1` (the CLI can't set a probe). MCP: `sevalla_api_update` with `updateProcessV3` and the same body.

**8. Connect the database**

[Create internal connection](https://api-docs.sevalla.com/v3/applications/create-internal-connection) (`createApplicationInternalConnectionV3`). Unlike the dashboard, it adds no variables:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST "$API/applications/$APP_ID/internal-connections" -H "$AUTH" -H "Content-Type: application/json" \
  -d "$(jq -n --arg t "$DB_ID" '{target_id:$t, target_type:"database"}')"
```

CLI: `sevalla apps internal-connections create $APP_ID --target-id $DB_ID --target-type database`. MCP: `sevalla_api_create` with `createApplicationInternalConnectionV3`.

**9. Add the environment variables**

[Create environment variable](https://api-docs.sevalla.com/v3/applications/create-environment-variable) (`createEnvironmentVariableV3`), once per variable. Store `N8N_ENCRYPTION_KEY` somewhere safe before you send it:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
N8N_ENCRYPTION_KEY=$(openssl rand -base64 32)
add_var() {
  curl -s -X POST "$API/applications/$APP_ID/env-vars" -H "$AUTH" -H "Content-Type: application/json" \
    -d "$(jq -n --arg k "$1" --arg v "$2" '{key:$k, value:$v, is_runtime:true, is_buildtime:false}')" | jq -r .key
}
add_var DB_TYPE postgresdb
add_var DB_POSTGRESDB_HOST "$DB_HOST"
add_var DB_POSTGRESDB_PORT 5432
add_var DB_POSTGRESDB_DATABASE n8n
add_var DB_POSTGRESDB_USER n8n
add_var DB_POSTGRESDB_PASSWORD "$DB_PASSWORD"
add_var N8N_ENCRYPTION_KEY "$N8N_ENCRYPTION_KEY"
add_var N8N_PORT 8080
add_var N8N_WEBHOOK_URL "https://$APP_HOST/"
add_var N8N_PROXY_HOPS 2
add_var N8N_DEFAULT_BINARY_DATA_MODE database
add_var N8N_GRACEFUL_SHUTDOWN_TIMEOUT 20
```

Each call prints the key it added. CLI: `sevalla apps env-vars create $APP_ID --key DB_TYPE --value postgresdb`, once per variable; the CLI makes each variable available during runtime and build, which makes no difference for an image. MCP: `set_environment_variables` with all of them in `variables`.

**10. Deploy**

[Trigger deployment](https://api-docs.sevalla.com/v3/applications/trigger-deployment) (`triggerDeploymentV3`):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DEPLOYMENT_ID=$(curl -s -X POST "$API/applications/$APP_ID/deployments" -H "$AUTH" -H "Content-Type: application/json" -d '{}' | jq -r .id)
```

CLI: `sevalla apps deployments trigger $APP_ID`. MCP: `trigger_deployment`.

**11. Wait for the deployment**

[Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) until `status` is final (`success`, `failed`, `cancelled` or `skipped`), for up to 30 minutes; [Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`) shows why one failed:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for i in $(seq 1 120); do
  STATUS=$(curl -s "https://api.sevalla.com/v3/applications/$APP_ID/deployments/$DEPLOYMENT_ID" \
    -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.status // empty')
  case $STATUS in success|failed|cancelled|skipped) break ;; esac
  sleep 15
done
echo "$STATUS"
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
success
```

CLI: `sevalla apps deployments get $DEPLOYMENT_ID --app-id $APP_ID` and `sevalla apps deployments logs $DEPLOYMENT_ID --app-id $APP_ID`. MCP: `get_deployment` and `get_deployment_logs`.

**12. Check n8n**

The readiness endpoint answers, and [Get application runtime logs](https://api-docs.sevalla.com/v3/applications/get-application-runtime-logs) (`getApplicationRuntimeLogsV3`) shows the URL n8n uses:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -w "\n" "https://$APP_HOST/healthz/readiness"
curl -s "$API/applications/$APP_ID/runtime-logs?search=$APP_HOST&order=desc&limit=1" -H "$AUTH" | jq -r '.[0].message'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"status":"ok"}
https://n8n-x7k2m.sevalla.app
```

CLI: `sevalla apps logs runtime $APP_ID --search $APP_HOST`. MCP: `search_runtime_logs`.

Then open `https://$APP_HOST` in a browser and [create the owner account](#deploy-n8n) at once: n8n has no API for the first account.

### Add queue mode with the API

**1. Create Valkey**

[Create database](https://api-docs.sevalla.com/v3/databases/create-database) (`createDatabaseV3`), then wait for `ready` as before:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
QUEUE_PASSWORD=$(openssl rand -hex 16)
QUEUE_ID=$(curl -s -X POST "$API/databases" -H "$AUTH" -H "Content-Type: application/json" \
  -d "$(jq -n --arg c "$CLUSTER_ID" --arg r "$DB1" --arg p "$QUEUE_PASSWORD" \
    '{display_name:"n8n-queue", type:"valkey", version:"9.0", cluster_id:$c, resource_type_id:$r, db_name:"n8n-queue", db_password:$p}')" \
  | jq -r .id)
for i in $(seq 1 60); do [ "$(curl -s "$API/databases/$QUEUE_ID" -H "$AUTH" | jq -r .status)" = ready ] && break; sleep 10; done
QUEUE_HOST=$(curl -s "$API/databases/$QUEUE_ID" -H "$AUTH" | jq -r .internal_hostname)
```

CLI: `sevalla databases create --name n8n-queue --type valkey --db-version 9.0 --cluster $CLUSTER_ID --resource-type $DB1 --db-name n8n-queue --db-password $QUEUE_PASSWORD`. MCP: `sevalla_api_create` with `createDatabaseV3`.

**2. Connect it**

[Create internal connection](https://api-docs.sevalla.com/v3/applications/create-internal-connection) (`createApplicationInternalConnectionV3`):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST "$API/applications/$APP_ID/internal-connections" -H "$AUTH" -H "Content-Type: application/json" \
  -d "$(jq -n --arg t "$QUEUE_ID" '{target_id:$t, target_type:"database"}')"
```

CLI: `sevalla apps internal-connections create $APP_ID --target-id $QUEUE_ID --target-type database`. MCP: `sevalla_api_create` with `createApplicationInternalConnectionV3`.

**3. Add the queue variables**

[Create environment variable](https://api-docs.sevalla.com/v3/applications/create-environment-variable) (`createEnvironmentVariableV3`), with the `add_var` function from before:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
add_var EXECUTIONS_MODE queue
add_var QUEUE_BULL_REDIS_HOST "$QUEUE_HOST"
add_var QUEUE_BULL_REDIS_PORT 6379
add_var QUEUE_BULL_REDIS_PASSWORD "$QUEUE_PASSWORD"
add_var OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS true
add_var N8N_WORKER_SUSPEND_EXECUTIONS_ON_SHUTDOWN true
```

CLI: `sevalla apps env-vars create`, once per variable. MCP: `set_environment_variables`.

**4. Create the worker**

[Create process](https://api-docs.sevalla.com/v3/applications/create-process) (`createProcessV3`). Every process needs `scaling_strategy` and `resource_type_id`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST "$API/applications/$APP_ID/processes" -H "$AUTH" -H "Content-Type: application/json" \
  -d "$(jq -n --arg r "$S1" '{display_name:"Worker", type:"worker", entrypoint:"exec n8n worker --concurrency=10", resource_type_id:$r, scaling_strategy:{type:"manual", config:{instanceCount:1}}}')" \
  | jq -r .entrypoint
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
exec n8n worker --concurrency=10
```

CLI: `sevalla apps processes create $APP_ID --name Worker --type worker --resource-type $S1 --command "exec n8n worker --concurrency=10"`. MCP: `sevalla_api_create` with `createProcessV3` and the same body.

**5. Deploy and check the worker**

Trigger and wait for a deployment as before. Workers have no readiness probe, so the deployment can succeed a few seconds before the worker is ready: search the runtime logs until it says so.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for i in $(seq 1 30); do [ "$(curl -s "$API/applications/$APP_ID/runtime-logs?search=worker%20is%20now%20ready&order=desc&limit=1" -H "$AUTH" | jq -r '.[0].message // empty')" = "n8n worker is now ready" ] && break; sleep 5; done
echo "n8n worker is now ready"
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
n8n worker is now ready
```

CLI: `sevalla apps logs runtime $APP_ID --search "worker is now ready"`. MCP: `search_runtime_logs`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.