# How to Run n8n in a Cloud Sandbox With a Working Webhook URL

> Run n8n's Docker image in a Sandboxer sandbox with a volume and --restart unless-stopped, set N8N_HOST, N8N_PROTOCOL and N8N_WEBHOOK_URL to the hosted address, then run sbx host 5678. The editor and webhooks answer on https://<subdomain>-5678.box.sandboxer.com, private by default; go public only for callers that can't send a token.

Source: https://sandboxer.com/guides/run-n8n-in-a-cloud-sandbox. Updated 2026-10-27.

**Short answer:** Run n8n's Docker image in a Sandboxer sandbox with a volume and --restart unless-stopped, set N8N_HOST, N8N_PROTOCOL and N8N_WEBHOOK_URL to the hosted address, then run sbx host 5678. The editor and webhooks answer on https://<subdomain>-5678.box.sandboxer.com, private by default; go public only for callers that can't send a token.

## At a glance

Everything here ran on one `small` sandbox (2 vCPU, 4 GB) on October 27, 2026, with n8n 2.45.4. Single runs, not benchmarks.

| Step | What we saw |
| - | - |
| `sbx new --type small` | Ready in 3.2 seconds |
| n8n start with an existing volume | "Editor is now accessible" after 17.4 seconds |
| Memory at idle | 731 MiB |
| Hosted URL, no token | `401` from the Sandboxer proxy |
| Hosted URL with the token | `302`, then the n8n editor (`<title>n8n.io - Workflow Automation</title>`) |
| Webhook call through the hosted URL | Median 225 ms over 20 calls from outside (each a new TLS connection), 64 ms from inside the sandbox |
| `sbx stop --wait`, then `sbx resume` | `memoryRestored: true`, same n8n PID, webhook answered 223 ms after resume returned |
| Cold boot (resume as `default`) | `memoryRestored: false`, Docker restarted n8n, webhook answered after 16.9 seconds, workflow and owner account kept |

## Start n8n with the hosted address

n8n's [Docker install docs](https://docs.n8n.io/hosting/installation/docker/) start the container with a `n8n_data` volume mounted at `/home/node/.n8n`. We used the same image and variables, swapped `-it --rm` for `-d --restart unless-stopped` so it survives reboots, and added the URL settings.

A sandbox's hosted URL is `https://<subdomain>-<port>.box.sandboxer.com`, and `sbx new` prints the subdomain. Ours was `silent-stormy-mint`, so n8n's address is `https://silent-stormy-mint-5678.box.sandboxer.com`. Set it before you start n8n:

```bash
sbx new --name n8n-demo --type small
id=sb_68ufa2y5te                                   # from sbx new
N8N_URL=silent-stormy-mint-5678.box.sandboxer.com  # <subdomain>-5678.box.sandboxer.com

sbx exec $id "docker volume create n8n_data"
sbx exec $id --timeout 590 "docker pull docker.n8n.io/n8nio/n8n"
sbx exec $id "docker run -d --name n8n --restart unless-stopped \
  -p 5678:5678 \
  -e GENERIC_TIMEZONE=UTC -e TZ=UTC \
  -e N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true \
  -e N8N_HOST=$N8N_URL -e N8N_PROTOCOL=https \
  -e N8N_WEBHOOK_URL=https://$N8N_URL/ \
  -e N8N_PROXY_HOPS=1 \
  -v n8n_data:/home/node/.n8n \
  docker.n8n.io/n8nio/n8n"
sbx exec $id "docker logs n8n 2>&1 | grep -A1 'Editor is now'"
```

```text
Editor is now accessible via:
https://silent-stormy-mint-5678.box.sandboxer.com
```

Why each URL setting matters, per n8n's [webhook URL docs](https://docs.n8n.io/hosting/configuration/configuration-examples/webhook-url/): n8n builds webhook URLs from `N8N_PROTOCOL`, `N8N_HOST` and `N8N_PORT`. Behind a proxy that serves port 443, that gives the wrong address, so set the webhook URL yourself and `N8N_PROXY_HOPS=1`. Sandboxer's hosting proxy is that proxy.

Use `N8N_WEBHOOK_URL`, not `WEBHOOK_URL`. We first started with `WEBHOOK_URL`, and n8n 2.45.4 logged:

```text
There are deprecations related to your n8n setup. Please take the recommended actions to update your configuration:
 - WEBHOOK_URL -> Use N8N_WEBHOOK_URL instead, which sets the base URL for both test and production webhooks.
```

The log also said the Python task runner couldn't start because the image has no Python 3. That comes from n8n's image, not the sandbox; our test workflow has no Code node.

The pull can exceed `sbx exec`'s default wait, so give it `--timeout` (up to 600 seconds) or `--detach` it.

## Host port 5678 and open the editor

```bash
sbx host $id 5678 --title n8n
```

The URL it prints ends in `?_token=<token>`. That token gates the whole port at the Sandboxer proxy:

```bash
B=https://silent-stormy-mint-5678.box.sandboxer.com
curl -s -o /dev/null -w "%{http_code}\n" $B/
# 401
curl -s -D - -o /dev/null -c jar.txt "$B/?_token=<token>" | grep -iE "^HTTP|^location"
# HTTP/2 302
# location: /
curl -s -b jar.txt $B/ | grep -o "<title>[^<]*</title>"
# <title>n8n.io - Workflow Automation</title>
curl -s -b jar.txt $B/healthz
# {"status":"ok"}
```

In a browser, open the full token link once. It sets a 30-day cookie and the editor loads.

n8n has its own login on top. On a fresh volume, `/rest/settings` reported `"showSetupOnFirstLoad":true`: the first person to open the editor creates the owner account. Create it while the port is still private. Behind the token, n8n's own API answered `{"status":"error","message":"Unauthorized"}` until we signed in.

## Import a webhook workflow with the n8n CLI

The workflow has two nodes: a Webhook trigger on `POST /webhook/hello`, and a Set node whose output becomes the response.

```json
[
  {
    "id": "helloWebhook01",
    "name": "Hello webhook",
    "active": false,
    "nodes": [
      {
        "id": "9f1c2a3b-0000-4000-8000-000000000001",
        "name": "Webhook",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 2,
        "position": [0, 0],
        "webhookId": "9f1c2a3b-0000-4000-8000-0000000000aa",
        "parameters": { "httpMethod": "POST", "path": "hello", "responseMode": "lastNode", "options": {} }
      },
      {
        "id": "9f1c2a3b-0000-4000-8000-000000000002",
        "name": "Reply",
        "type": "n8n-nodes-base.set",
        "typeVersion": 3.4,
        "position": [220, 0],
        "parameters": {
          "assignments": { "assignments": [
            { "id": "a1", "name": "greeting", "type": "string", "value": "={{ 'hello ' + $json.body.name }}" },
            { "id": "a2", "name": "receivedAt", "type": "string", "value": "={{ $now.toISO() }}" }
          ] },
          "options": {}
        }
      }
    ],
    "connections": { "Webhook": { "main": [[{ "node": "Reply", "type": "main", "index": 0 }]] } },
    "settings": {}
  }
]
```

Copy it in, import it, publish it and restart n8n. Per n8n's [CLI docs](https://docs.n8n.io/hosting/cli-commands/), `import:workflow` deactivates what it imports, `publish:workflow` replaces the deprecated `update:workflow --active=true` from n8n 2.0, and CLI changes take effect only after a restart.

```bash
sbx scp hello-webhook.json $id:/home/user/hello-webhook.json
sbx exec $id "docker cp hello-webhook.json n8n:/tmp/hello-webhook.json"
sbx exec $id "docker exec n8n n8n import:workflow --input=/tmp/hello-webhook.json"
sbx exec $id "docker exec n8n n8n publish:workflow --id=helloWebhook01"
sbx exec $id "docker restart n8n"
```

```text
Importing 1 workflows...
Successfully imported 1 workflow.
Publishing workflow with ID: helloWebhook01 (current version)
Note: Changes will not take effect if n8n is running.
…
Finished building workflow dependency index. Processed 0 draft workflows, 1 published workflows.
```

## Call the webhook from outside

On the private port, a caller has to carry the token. We tried three ways:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST -H "Content-Type: application/json" \
  -d '{"name":"Ada"}' $B/webhook/hello
# 401

curl -s -X POST -H "Content-Type: application/json" \
  -d '{"name":"Grace"}' "$B/webhook/hello?_token=<token>"
# {"greeting":"hello Grace","receivedAt":"2026-10-27T09:44:46.401Z"}   (200)

curl -s -b jar.txt -X POST -H "Content-Type: application/json" \
  -d '{"name":"Ada"}' $B/webhook/hello
# {"greeting":"hello Ada","receivedAt":"2026-10-27T09:44:39.365Z"}   (200)
```

A `POST` with `?_token=` in the URL went straight through to n8n with a `200` and also set the cookie. (A `GET` with the token gets a `302` first, as above.) So a sender that lets you paste a full URL can call a private port. Then we made the port public:

```bash
sbx host $id 5678 --public --title n8n
curl -s -X POST -H "Content-Type: application/json" -d '{"name":"Linus"}' $B/webhook/hello
# {"greeting":"hello Linus","receivedAt":"2026-10-27T09:45:09.256Z"}
curl -s $B/rest/workflows
# {"status":"error","message":"Unauthorized"}
curl -s -X POST $B/webhook/nope
# {"code":404,"message":"The requested webhook \"POST nope\" is not registered.", …}
```

With the port public, anyone can reach the editor's sign-in page and every webhook path. n8n's own login still guards the editor and API.

## Private or public: which to pick

| | Private port (default) | Public port |
| - | - | - |
| Webhook URL you give the sender | `https://…-5678.box.sandboxer.com/webhook/hello?_token=<token>` | `https://…-5678.box.sandboxer.com/webhook/hello` |
| Request without the token | `401` from Sandboxer, n8n never sees it | Reaches n8n |
| Who can reach the editor | Only people with the token, then n8n's login | Anyone, then n8n's login |
| What the token gives away | The whole port, editor included: a sender that stores your webhook URL holds it | Nothing to give away |
| Senders that reject query strings | Can't call it | Work |
| Protect the webhook itself | The token already does | Use the Webhook node's Basic, Header or JWT auth ([n8n docs](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/)) |

Keep the port private while you set up the owner account and build workflows. If you only test with your own scripts, stay private. Make it public when an outside service has to call in and can't carry the token, and turn on authentication in each Webhook node. Switching public to private later mints a new token: in our run the old link returned `401` and the new one `302`. Update the URL anywhere you pasted the old one.

## What survives stop and resume

We noted n8n's container PID (2013) and ran:

```bash
sbx stop $id --wait     # 2.2 s
sbx resume $id          # 1.4 s
sbx info $id            # memoryRestored: true
```

While stopped, the webhook URL returned `404`. After resume, n8n answered 223 ms after `sbx resume` returned, with the same PID and start time: the process never restarted.

Memory is restored only when the resume lands on the same CPU type. To see the other case, we stopped again and resumed with `--type default`, which always boots from disk:

```text
memoryRestored: false, resumeReason: resized
/proc/uptime: 19.66
n8n PID 615, started 2026-10-27T09:45:46Z
{"greeting":"hello after cold boot","receivedAt":"2026-10-27T09:46:04.498Z"}
```

Docker is an enabled service, so it started at boot and `--restart unless-stopped` brought n8n back. The webhook answered 16.9 seconds after resume returned. The workflow was still published and the owner account still existed (`showSetupOnFirstLoad: false`), because both live in SQLite on the `n8n_data` volume. Without the restart policy, n8n stays down after a cold boot until you start it. Hosted routes come back either way. You can move to a bigger size on resume. Moving to a smaller size isn't supported yet (create a new sandbox and copy), so pick `small` or `default` up front.

## FAQ

### Which size does n8n need?

`small` (2 vCPU, 4 GB, $0.018 per hour) ran this test with n8n using 731 MiB at idle. Use `default` for heavy workflows or many executions at once.

### Why does the editor show the wrong webhook URL?

`N8N_WEBHOOK_URL` isn't set, or still points at `localhost`. Set it to `https://<subdomain>-5678.box.sandboxer.com/` and recreate the container. In n8n 2.35 and later, `WEBHOOK_URL` is deprecated.

### Does the n8n URL change after stop, resume or resize?

No. The subdomain and hosted routes are kept. A fork gets a new subdomain and no hosted ports, so set the n8n URL variables again there.

### Do I need n8n's tunnel option?

No. n8n's tunnel is for local development. The hosted URL already reaches the sandbox over HTTPS.

## Related

- [Give a sandbox port an HTTPS URL](/guides/give-a-sandbox-port-an-https-url)
- [Run Docker Compose in a sandbox with a URL per service](/guides/run-docker-compose-in-a-sandbox-with-a-url-per-service)
- [Pause and resume a sandbox with its memory](/guides/pause-and-resume-a-sandbox-with-memory)
- [Hosting](/docs/hosting)
