Skip to content

Self-host n8n 3.0

The next n8n on its own VM. Try 3.0 before it's stable.

Deploy on exe.dev

Shelley handles the setup. About 15 minutes.

Prefer a terminal?
curl -fsSL https://exe-staging.dev/deploy/n8n-3/prompt.txt | ssh exe.dev new --prompt=/dev/stdin

Warning

n8n 3.0 is a beta, not a stable release yet. n8n expects it to be stable in early December 2026. For the stable 2.x setup, see Self-host n8n.

n8n connects your apps into little chains of “when this happens, do that.” Version 3.0 clears out old nodes, gives Code nodes a one-minute limit, and rotates the keys behind your saved credentials. Try it on a fresh VM, or upgrade the VM you set up with our n8n 2.x recipe.

What you get#

  • n8n 3.0 on its own VM, with a private editor and webhooks you can make public.
  • A Hello webhook, a five-minute Heartbeat, and ready-to-use AI credentials.
  • Nightly backups, including the key that unlocks your saved credentials.
  • A backup restored and checked before you start.
What's running, and where
Private https://my-n8n-3.exe.xyz:5678/ Your editor
Public https://my-n8n-3.exe.xyz/webhook/ Workflow endpoints, only after you enable public access
my-n8n-3.exe.xyz exe.dev VM
n8n 3.0 beta Workflow editor and engine :5678~/n8n/n8n-data
Task runners JavaScript and Python, one minute per Code node run
PostgreSQL 18 Workflows, credentials, encryption keys, and history ~/n8n/postgres
Webhook proxy nginx, keeping the editor off the public URL :8000

Deploy with Shelley#

Click Deploy and Shelley will set up n8n 3.0 on a new VM, then check that it works. No config-file wrangling required.

Prefer a terminal?
curl -fsSL \
  https://exe-staging.dev/deploy/n8n-3/prompt.txt \
  | ssh exe.dev new --prompt=/dev/stdin
What Shelley is told
Set up n8n 3.0 on this VM by following the exe.dev recipe at https://exe-staging.dev/deploy/n8n-3.md from top to bottom.

The recipe's example VM name is my-n8n-3. This VM's name is the output of `hostname`: use it wherever the recipe writes my-n8n-3. Skip the step that creates the VM; you are on it.

You are done when all of these are true. Check each one yourself:
- n8n 3.0 on its own VM, with a private editor and webhooks you can make public.
- A Hello webhook, a five-minute Heartbeat, and ready-to-use AI credentials.
- Nightly backups, including the key that unlocks your saved credentials.
- A backup restored and checked before you start.

Read the whole recipe, including the closed details. For initial setup,
follow "Install by hand" first: containers, example workflows, AI
credentials, backup timer, and scratch restore check. The opening
walkthrough is for the owner afterwards. Do not run the Upgrade, Restore,
or Update instructions during initial setup.
Work as exedev in ~/n8n. Never overwrite an existing ~/n8n/.env: it
holds the encryption key. Check it matches n8n-data/config, and that
N8N_VERSION is 3.x.
Do not create the owner account; the owner does that in the browser.
Leave public sharing off. Verify the Hello webhook at localhost:8000
and confirm / and /rest/settings return 404 there. Wait up to five
minutes for a successful Heartbeat execution. Check both AI credentials.
Verify docker.service is enabled and all containers use unless-stopped;
do not reboot. Restore the backup only in the scratch directory, check
credentials decrypt, then remove the scratch stack.
Finish with the private editor URL, a reminder that n8n 3.0 is a beta,
and the OPTIONAL command the owner can run on their own computer to
allow public webhooks:
ssh exe.dev share set-public my-n8n-3

Finish with the URLs I should open.

Also at /deploy/n8n-3/prompt.txt. The recipe it follows is /deploy/n8n-3.md.

Open the editor#

When Shelley finishes, open the editor URL it gives you. It'll look like https://my-n8n-3.exe.xyz:5678/.

Sign in to exe.dev, then create your n8n owner account. Two logins, two locks. Turn on two-factor authentication in Settings → Personal, too.

Make something happen#

You'll find two starter workflows waiting for you:

  • Hello webhook echoes back whatever you send it. Try it below.
  • Heartbeat runs every five minutes. Open Executions to see it ticking, then replace its Code node with something useful—or unpublish it.

Ready for your own? Follow n8n's first-workflow tutorial or pick a template. Save, Publish, and off it goes. Older templates may use nodes that 3.0 removed; n8n won't publish those. Schedules start in UTC, and a Code node gets one minute per run.

Let the outside world in#

Only using schedules? Skip this bit. To accept calls from other apps, you can make your webhook, form, and MCP endpoints public. The editor stays private. Anyone with an unprotected endpoint's URL can call it, so add Authentication to Webhook nodes that do real work.

On your computer, using your VM's name:

ssh exe.dev share set-public my-n8n-3
curl -X POST https://my-n8n-3.exe.xyz/webhook/hello \
  -H 'Content-Type: application/json' -d '{"hello":"world"}'

You'll get "ok":true and your message back. Hello, automation. To close the door again: ssh exe.dev share set-private my-n8n-3.

Keep your work#

Backups run nightly into ~/n8n-backups. Keep a copy off the VM—a backup on a lost VM won't help much. On your computer:

scp -r my-n8n-3.exe.xyz:n8n-backups .

Keep these files private: they include the key for your saved credentials. Don't replace ~/n8n/.env; losing that key makes those credentials unreadable. Something stuck? On the VM, try cd ~/n8n && docker compose logs --tail=100.

Coming from n8n 2.x?#

Upgrading is one way: 3.0 migrates the database and changes how credentials are encrypted, so the only way back is a backup. Before you start:

  • Check your workflows. On 2.x, open Settings → Migration Report. It lists workflows that will break, such as ones using the removed Function, Cron, Interval, or Item Lists nodes, and can migrate some for you. Removed nodes stop a workflow from running on 3.0.
  • Mind the one-minute limit. Code nodes that need longer fail on 3.0 unless you raise the limit.
  • Slack Trigger workflows need the app's signing secret in their Slack credential, or they stop.

Then take a backup and upgrade, as below. Your credentials, workflows, and execution history come along. For everything else, read n8n's breaking changes.

The nuts and bolts#

Happy clicking Deploy? You can stop here. These are the same setup steps Shelley follows, plus the upgrade and maintenance commands for later.

Install by hand

Create a VM#

On your computer (pick another name if my-n8n-3 is taken):

ssh exe.dev new --name=my-n8n-3
ssh my-n8n-3.exe.xyz

All remaining installation commands run on the VM, as exedev. Commands use hostname, so your VM can have any name.

Install n8n with PostgreSQL#

Enable Docker at boot; otherwise exe.dev's on-demand Docker won't restart these containers after a reboot. Install jq if needed:

sudo systemctl enable --now docker.service
systemctl is-enabled docker.service
command -v jq >/dev/null || sudo apt-get install -y jq

The check should print enabled. Create the directories and generate the secrets once. This leaves an existing .env alone and pins n8n and its runners to the same 3.x release. During the beta that's the newest 3.x image on Docker Hub; n8n's latest and beta tags still mean 2.x.

mkdir -p ~/n8n/n8n-data ~/n8n/postgres ~/n8n/workflows
cd ~/n8n
if [ ! -e .env ]; then
  N8N_VERSION=$(curl -fsSL 'https://hub.docker.com/v2/repositories/n8nio/runners/tags?page_size=100&name=3.' \
    | jq -r '[.results[].name | select(test("^3\\.[0-9]+\\.[0-9]+$"))] | sort_by(split(".") | map(tonumber)) | last // empty')
  case "$N8N_VERSION" in 3.*) ;; *) N8N_VERSION=3.0.1 ;; esac
  touch .env && chmod 600 .env
  cat >> .env <<EOF
# n8n settings and secrets. Keep a copy somewhere safe: without
# N8N_ENCRYPTION_KEY, the credentials saved in n8n can't be decrypted.
N8N_VERSION=$N8N_VERSION
N8N_HOSTNAME=$(hostname).exe.xyz
GENERIC_TIMEZONE=UTC
N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)
POSTGRES_PASSWORD=$(openssl rand -hex 24)
N8N_RUNNERS_AUTH_TOKEN=$(openssl rand -hex 32)
EOF
fi
grep ^N8N_VERSION= .env

The last line confirms the version: 3.0.1 or newer, never 4.x. Schedules default to UTC; change GENERIC_TIMEZONE in .env if you'd like a different default.

Create the four-service Compose stack:

cat > ~/n8n/compose.yaml <<'EOF'
# n8n + PostgreSQL + task runners + a webhook-only proxy.
# Secrets and versions come from .env in this directory.
name: n8n

services:
  postgres:
    image: postgres:18
    restart: unless-stopped
    environment:
      POSTGRES_USER: n8n
      POSTGRES_DB: n8n
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - ./postgres:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
      interval: 5s
      timeout: 5s
      retries: 10

  n8n:
    image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
    restart: unless-stopped
    ports:
      - "5678:5678"
    environment:
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: postgres
      DB_POSTGRESDB_DATABASE: n8n
      DB_POSTGRESDB_USER: n8n
      DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
      N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
      N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: "true"
      N8N_EDITOR_BASE_URL: https://${N8N_HOSTNAME}:5678/
      N8N_WEBHOOK_URL: https://${N8N_HOSTNAME}/
      N8N_PROXY_HOPS: "1"
      GENERIC_TIMEZONE: ${GENERIC_TIMEZONE}
      TZ: ${GENERIC_TIMEZONE}
      N8N_RUNNERS_MODE: external
      N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN}
      N8N_RUNNERS_BROKER_LISTEN_ADDRESS: 0.0.0.0
      N8N_RUNNERS_TASK_TIMEOUT: ${N8N_RUNNERS_TASK_TIMEOUT:-60}
    volumes:
      - ./n8n-data:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy

  runners:
    image: n8nio/runners:${N8N_VERSION}
    restart: unless-stopped
    environment:
      N8N_RUNNERS_TASK_BROKER_URI: http://n8n:5679
      N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN}
      N8N_RUNNERS_TASK_TIMEOUT: ${N8N_RUNNERS_TASK_TIMEOUT:-60}
    depends_on:
      - n8n

  webhooks:
    image: nginx:stable-alpine
    restart: unless-stopped
    ports:
      - "8000:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - n8n
EOF

n8n 3.0 stops a Code node after 60 seconds. Both n8n and the runners read the limit, so it lives in one place: to allow five minutes, add N8N_RUNNERS_TASK_TIMEOUT=300 to .env and run docker compose up -d.

exe.dev's HTTPS proxy handles TLS and login. The editor is private on port 5678; this nginx configuration exposes only workflow endpoints on port 8000. It preserves the forwarding headers and rejects non-workflow paths, including encoded paths that try to bypass the check.

cat > ~/n8n/nginx.conf <<'EOF'
# Port 8000 is the only port exe.dev can make public. Serve n8n's
# webhook, form, and MCP endpoints there, and nothing else.
server {
    listen 80 default_server;
    server_tokens off;

    # Look n8n up at request time, so a recreated container is found.
    resolver 127.0.0.11 valid=10s ipv6=off;
    set $n8n http://n8n:5678;

    client_max_body_size 200m;
    proxy_http_version 1.1;
    proxy_buffering off;
    chunked_transfer_encoding off;
    proxy_read_timeout 300s;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    # Pass exe.dev's forwarding headers through unchanged, so n8n sees
    # one proxy (exe.dev) here and on the editor port: N8N_PROXY_HOPS=1.
    proxy_set_header X-Forwarded-For $http_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $http_x_forwarded_host;
    proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;

    location ~ ^/(webhook|webhook-test|webhook-waiting|form|form-test|form-waiting|mcp|mcp-test)/ {
        # Check the raw path too, so encoded tricks like
        # /rest/x/..%2F..%2Fwebhook/ can't reach anything else.
        if ($request_uri !~ ^/(webhook|webhook-test|webhook-waiting|form|form-test|form-waiting|mcp|mcp-test)/) {
            return 404;
        }
        proxy_pass $n8n;
    }

    location / {
        return 404;
    }
}
EOF

Start the stack and wait for {"status":"ok"}. If this takes more than five minutes, stop the loop with Ctrl-C and check the logs.

cd ~/n8n
docker compose up -d
until curl -fs http://localhost:5678/healthz/readiness; do sleep 5; done; echo
docker compose ps

All four services should be Up, with PostgreSQL healthy. Confirm its workflow, credential, and execution tables exist:

cd ~/n8n
docker compose exec postgres psql -U n8n -d n8n -c '\dt' | grep -E 'credentials_entity|execution_entity|workflow_entity'

Keep the encryption key safe#

The key encrypts saved credentials, not the whole database. Keep .env in your password manager as well as your backups. Check both copies of the key agree:

cd ~/n8n
stat -c '%a %n' .env n8n-data/config
[ "$(jq -r .encryptionKey n8n-data/config)" = "$(sed -n 's/^N8N_ENCRYPTION_KEY=//p' .env)" ] && echo "keys match"

Expect 600 for each file and keys match. Never generate a replacement key for an existing installation.

Add a webhook and a schedule#

Create Hello webhook, which echoes a POST request's JSON:

cat > ~/n8n/workflows/hello-webhook.json <<'EOF'
{
  "id": "helloWebhook0001",
  "name": "Hello webhook (example)",
  "nodes": [
    {
      "id": "9a3b5f0e-4c1d-4e8a-9b7c-1f2e3d4c5b6a",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [0, 0],
      "webhookId": "5d0f3c8e-2b7a-4f6e-8c1d-3a9b7e5f2c40",
      "parameters": {
        "httpMethod": "POST",
        "path": "hello",
        "responseMode": "responseNode",
        "options": {}
      }
    },
    {
      "id": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [240, 0],
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ ok: true, received: $json.body, at: $now.toISO() }) }}",
        "options": {}
      }
    }
  ],
  "connections": {
    "Webhook": {
      "main": [[{ "node": "Respond to Webhook", "type": "main", "index": 0 }]]
    }
  },
  "settings": { "executionOrder": "v1" },
  "active": false
}
EOF

Create Heartbeat, which also tests JavaScript execution in the task runner:

cat > ~/n8n/workflows/heartbeat-schedule.json <<'EOF'
{
  "id": "heartbeatSched01",
  "name": "Heartbeat every 5 minutes (example)",
  "nodes": [
    {
      "id": "7e6d5c4b-3a29-4180-9f7e-6d5c4b3a2918",
      "name": "Every 5 minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [0, 0],
      "parameters": {
        "rule": { "interval": [{ "field": "minutes", "minutesInterval": 5 }] }
      }
    },
    {
      "id": "2b3c4d5e-6f70-4812-93a4-b5c6d7e8f901",
      "name": "Heartbeat",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [240, 0],
      "parameters": {
        "jsCode": "return [{ json: { heartbeat: new Date().toISOString() } }];"
      }
    }
  ],
  "connections": {
    "Every 5 minutes": {
      "main": [[{ "node": "Heartbeat", "type": "main", "index": 0 }]]
    }
  },
  "settings": { "executionOrder": "v1" },
  "active": false
}
EOF

Import, publish, and restart so n8n picks them up. If the owner account isn't set up yet, these workflows become yours when you create it.

cd ~/n8n
docker compose cp workflows n8n:/tmp/
docker compose exec n8n n8n import:workflow --separate --input=/tmp/workflows
docker compose exec n8n n8n publish:workflow --id=helloWebhook0001
docker compose exec n8n n8n publish:workflow --id=heartbeatSched01
docker compose restart n8n
until curl -fs http://localhost:5678/healthz/readiness; do sleep 5; done; echo

Check the webhook through the proxy:

curl -s -X POST http://localhost:8000/webhook/hello \
  -H 'Content-Type: application/json' -d '{"name":"exe.dev"}'; echo

Expect {"ok":true,"received":{"name":"exe.dev"},"at":"..."}. Both the editor and its API must return 404 on this port:

curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8000/
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8000/rest/settings

Within five minutes, check for a Heartbeat execution with mode trigger and status success:

cd ~/n8n
docker compose exec postgres psql -U n8n -d n8n -c 'SELECT e.id, w.name, e.mode, e.status, e."startedAt" FROM execution_entity e JOIN workflow_entity w ON w.id = e."workflowId" ORDER BY e.id DESC LIMIT 5;'

Use AI models without an API key#

Check the VM's LLM integration:

curl -s https://llm.int.exe.xyz/v1/models | jq '.data | length'

A number means it's connected; otherwise follow the integration's setup instructions first. Import these credentials. The key is a placeholder: exe.dev authenticates the VM, and model usage is billed through the integration.

cat > /tmp/exe-llm-credentials.json <<'EOF'
[
  {
    "id": "exeLlmOpenAi0001",
    "name": "exe.dev LLM (OpenAI API)",
    "type": "openAiApi",
    "data": { "apiKey": "exe-llm-integration", "url": "https://llm.int.exe.xyz/v1" }
  },
  {
    "id": "exeLlmClaude0001",
    "name": "exe.dev LLM (Anthropic API)",
    "type": "anthropicApi",
    "data": { "apiKey": "exe-llm-integration", "url": "https://llm.int.exe.xyz" }
  }
]
EOF
cd ~/n8n
docker compose cp /tmp/exe-llm-credentials.json n8n:/tmp/
docker compose exec n8n n8n import:credentials --input=/tmp/exe-llm-credentials.json

The matching OpenAI and Anthropic model nodes can now use these credentials.

Set up nightly backups#

This script saves the database, .env, n8n data, and configuration in one private archive. It keeps the 14 newest backups.

cat > ~/n8n/backup.sh <<'EOF'
#!/usr/bin/env bash
# Back up n8n into one tarball: a PostgreSQL dump, .env (with the
# encryption key), the Compose files, and n8n's data directory.
set -euo pipefail
umask 077
cd "$(dirname "$0")"
dest="$HOME/n8n-backups"
mkdir -p "$dest"
work=$(mktemp -d "$dest/.tmp-XXXXXX")
trap 'rm -rf "$work"' EXIT

docker compose exec -T postgres pg_dump -U n8n -d n8n --format=custom > "$work/n8n.dump"
cp -a .env compose.yaml nginx.conf backup.sh workflows n8n-data "$work/"

out="$dest/n8n-$(date -u +%Y%m%d-%H%M%S).tar.gz"
tar -czf "$out.part" -C "$work" .
chmod 600 "$out.part"
mv "$out.part" "$out"

# Keep the 14 newest backups.
ls -1t "$dest"/n8n-*.tar.gz | tail -n +15 | xargs -r rm --
echo "$out"
EOF
chmod +x ~/n8n/backup.sh

Schedule it, then take a first backup:

sudo tee /etc/systemd/system/n8n-backup.service >/dev/null <<EOF
[Unit]
Description=Back up n8n (database, .env, data) to ~/n8n-backups
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
User=$USER
ExecStart=$HOME/n8n/backup.sh
EOF
sudo tee /etc/systemd/system/n8n-backup.timer >/dev/null <<'EOF'
[Unit]
Description=Back up n8n every night

[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=30m
Persistent=true

[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now n8n-backup.timer
sudo systemctl start n8n-backup.service
systemctl list-timers n8n-backup.timer
ls -lh ~/n8n-backups

Check that the timer has a next run and the directory contains a tarball. For failures: journalctl -u n8n-backup.service.

Test a backup#

Restore the newest backup into a scratch stack, not the running one, and check that its credentials still decrypt:

latest=$(ls -1t ~/n8n-backups/n8n-*.tar.gz | head -n 1)
mkdir -p /tmp/n8n-restore-test/postgres
tar -xzf "$latest" -C /tmp/n8n-restore-test
cd /tmp/n8n-restore-test
docker compose -p n8n-restore-test up -d --wait postgres
docker compose -p n8n-restore-test exec -T postgres pg_restore -U n8n -d n8n --no-owner --exit-on-error < n8n.dump
docker compose -p n8n-restore-test run --rm --no-deps n8n list:workflow
docker compose -p n8n-restore-test run --rm --no-deps -T n8n export:credentials --all --decrypted 2>/dev/null \
  | tail -n 1 | jq -r '.[] | "\(.name): decrypted"'

Expect the workflow names and a decrypted line for each credential, including both LLM credentials. Remove only this scratch copy afterwards (and before repeating the test):

cd /tmp/n8n-restore-test && docker compose -p n8n-restore-test down
cd ~ && sudo rm -rf /tmp/n8n-restore-test

Installation done. Leave owner-account creation and public sharing to the owner; continue with Open the editor.

Upgrade a 2.x VM to 3.0

These steps upgrade a VM set up with our n8n 2.x recipe, here called my-n8n. Clear the Migration Report first, as described above.

Update to the latest 2.x#

n8n recommends upgrading from the newest 2.x release; its Migration Report knows the most. On the VM:

cd ~/n8n
grep ^N8N_VERSION= .env
curl -fsSL https://registry.npmjs.org/-/package/n8n/dist-tags | jq -r .stable

If the two versions differ, follow the 2.x recipe's Update n8n steps with the second one, then check the Migration Report again.

Back up#

Take a fresh backup, and copy it off the VM. It is your only way back to 2.x. On the VM:

~/n8n/backup.sh

On your computer:

scp -r my-n8n.exe.xyz:n8n-backups .

Note the newest file's name, such as n8n-20261030-210512.tar.gz. It holds the database, your data, and .env with its 2.x N8N_VERSION.

Tip

Want a dress rehearsal? ssh exe.dev cp my-n8n my-n8n-rehearsal copies the whole VM; upgrade the copy first. Its published workflows run too, so delete it when you're done: ssh exe.dev rm my-n8n-rehearsal.

Upgrade#

On the VM, pin the newest 3.x release and restart with it. Migrations take a minute or so; don't stop n8n while they run.

cd ~/n8n
v=$(curl -fsSL 'https://hub.docker.com/v2/repositories/n8nio/runners/tags?page_size=100&name=3.' \
  | jq -r '[.results[].name | select(test("^3\\.[0-9]+\\.[0-9]+$"))] | sort_by(split(".") | map(tonumber)) | last // empty')
case "$v" in 3.*) ;; *) v=3.0.1 ;; esac
sed -i "s/^N8N_VERSION=.*/N8N_VERSION=$v/" .env
grep ^N8N_VERSION= .env
docker compose pull
docker compose up -d
until curl -fs http://localhost:5678/healthz/readiness; do sleep 5; done; echo
docker compose exec n8n n8n --version

Expect {"status":"ok"} and the 3.x version. Look for workflows that didn't start, and check that saved credentials still decrypt:

cd ~/n8n
docker compose logs n8n | grep -A1 'cannot load'
docker compose exec -T n8n n8n export:credentials --all --decrypted 2>/dev/null \
  | tail -n 1 | jq -r '.[] | "\(.name): decrypted"'

No output from the first command is good news. Otherwise it names the removed nodes, and the Migration Report lists the workflows using them. Replace those nodes (Function becomes Code, Cron and Interval become Schedule Trigger), then publish each workflow again. Then run ~/n8n/backup.sh for a first 3.0 backup.

Code nodes now stop after a minute. To raise the limit, replace compose.yaml with the one above, add N8N_RUNNERS_TASK_TIMEOUT=300 to .env, and run docker compose up -d.

Go back to 2.x#

Changing N8N_VERSION back isn't supported. Instead, restore the backup you took before upgrading; its .env brings 2.x back with it. Anything that changed on 3.0 since is lost.

Back up and restore

On the VM, ~/n8n/backup.sh makes an extra backup and prints its path. Each archive includes the database, data directory, and encryption key; restore them together. On 3.0 the database holds your credentials' working keys, and .env holds the key that unlocks them. Copy backups off the VM as shown above.

Restore#

On a new VM, enable Docker and install jq as in Install. Create ~/n8n-backups there, then copy your backup over from your computer:

scp n8n-20261030-210512.tar.gz my-n8n-3.exe.xyz:n8n-backups/

Use your backup's actual filename in this and the commands below. All remaining commands run on the VM. If n8n already runs there, stop it and keep its directory aside first:

cd ~/n8n && docker compose down
mv ~/n8n ~/n8n.before-restore

Unpack the backup and set this VM's hostname:

mkdir ~/n8n
tar -xzf ~/n8n-backups/n8n-20261030-210512.tar.gz -C ~/n8n
cd ~/n8n
sed -i "s/^N8N_HOSTNAME=.*/N8N_HOSTNAME=$(hostname).exe.xyz/" .env
grep ^N8N_VERSION= .env

The backup brings its own n8n version, so a 2.x backup restores 2.x. Load the database before starting n8n:

cd ~/n8n
mkdir -p postgres
docker compose up -d --wait postgres
docker compose exec -T postgres pg_restore -U n8n -d n8n --no-owner --exit-on-error < n8n.dump
docker compose up -d
until curl -fs http://localhost:5678/healthz/readiness; do sleep 5; done; echo

Expect {"status":"ok"}. Repeat the key-match and local webhook checks from installation. On a new VM, recreate the backup timer too, and enable public sharing only if you need it. Keep ~/n8n.before-restore until you're happy everything is back.

Update n8n

Pick a newer 3.x version from n8n's release notes. Beta releases can change things between versions, so read each one. On the VM, back up, then edit only N8N_VERSION in .env; both n8n and its runners use it:

cd ~/n8n
./backup.sh
nano .env
docker compose pull
docker compose up -d
until curl -fs http://localhost:5678/healthz/readiness; do sleep 5; done; echo
docker compose exec n8n n8n --version

Check the reported version and try your workflows. To roll back, restore the backup—changing the image tag alone won't undo database migrations. When 3.0 goes stable, update to that release the same way.

Something not working?

On the VM:

cd ~/n8n
docker compose logs --tail=100 n8n runners postgres webhooks
  • The main URL returns 404: expected! Open the editor at https://my-n8n-3.exe.xyz:5678/; the main URL serves workflow endpoints only.
  • “Workflow cannot run on this instance”: it uses a node that 3.0 removed. Replace the node, then publish again.
  • “Task execution timed out after 60 seconds”: a Code node needed more than a minute. Raise the limit in .env; setting it on only one container doesn't work.
  • Webhook “not registered”: publish the workflow and check you're using POST and its Production URL. A Test URL needs a listening test run.
  • Encryption-key errors: restore .env, n8n-data, and the database from the same backup. Don't generate a new key.
  • Code nodes fail: check runner logs. Its version and auth token must match n8n's.
  • AI credentials blocked after turning on SSRF protection: add N8N_SSRF_ALLOWED_HOSTNAMES: llm.int.exe.xyz to the n8n service.
  • Down after reboot: run sudo systemctl enable --now docker.service.
  • EACCES on /home/node/.n8n: fix data ownership with sudo chown -R 1000:1000 ~/n8n/n8n-data.

Frequently asked questions#

Is n8n 3.0 stable?

Not yet. The beta came out on October 27, 2026, and n8n expects a stable release in early December. Until then, use n8n 2.x for anything that matters.

Can I go back to n8n 2.x?

Only by restoring a backup made before the upgrade. Changing the version number back isn't supported, and anything done on 3.0 since the backup is lost.

Will my 2.x workflows still run?

Most will. Workflows that use removed nodes, such as Function, Cron, or Item Lists, stop running. Before upgrading, check Settings → Migration Report on your 2.x instance.

Is n8n free?

The Community edition is free to self-host. You pay for your VM; see exe.dev plans. AI model usage is separate.

Can I use AI models?

Yes. Pick the included exe.dev LLM (OpenAI API) or exe.dev LLM (Anthropic API) credential in its matching model node. No provider key to paste; usage is billed through your LLM integration.

Updated Oct 30, 2026 · tested end to end Oct 30, 2026 · Markdown · manifest.json · Something wrong? Tell us