---
title: "Self-host n8n"
description: "Connect your apps with n8n on exe.dev. Deploy in a few clicks, open your private editor, and let your workflows run while your laptop naps."
url: https://hub.exe.dev/deploy/n8n
category: "Automation"
updated: 2026-10-14
---

# Self-host n8n

Connect your apps. Let the boring bits run themselves.

- **Software:** n8n (https://n8n.io)
- **Source:** https://github.com/n8n-io/n8n
- **License:** Sustainable Use License
- **Time:** 15 minutes
- **Example VM name:** `my-n8n` (yours will differ; on the VM, `hostname` prints it)
- **Machine-readable:** https://hub.exe.dev/deploy/n8n/manifest.json

## What you get

- Always-on n8n, 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.

Running on the VM:

- **n8n 2.x**: Workflow editor and engine, port 5678, data in `~/n8n/n8n-data`
- **Task runners**: Isolated JavaScript and Python execution
- **PostgreSQL 18**: Workflows, credentials, and history, data in `~/n8n/postgres`
- **Webhook proxy**: nginx, keeping the editor off the public URL, port 8000

How it is reached:

- https://my-n8n.exe.xyz:5678/ (private): Your editor
- https://my-n8n.exe.xyz/webhook/ (public): Workflow endpoints, only after you enable public access

## Deploy with Shelley

Open https://exe.dev/new?prompt=Set%20up%20n8n%20on%20this%20VM%20by%20following%20the%20exe.dev%20recipe%20at%20https%3A%2F%2Fhub.exe.dev%2Fdeploy%2Fn8n.md%20from%20top%20to%20bottom.%0A%0AThe%20recipe%27s%20example%20VM%20name%20is%20my-n8n.%20This%20VM%27s%20name%20is%20the%20output%20of%20%60hostname%60%3A%20use%20it%20wherever%20the%20recipe%20writes%20my-n8n.%20Skip%20the%20step%20that%20creates%20the%20VM%3B%20you%20are%20on%20it.%0A%0AYou%20are%20done%20when%20all%20of%20these%20are%20true.%20Check%20each%20one%20yourself%3A%0A-%20Always-on%20n8n%2C%20with%20a%20private%20editor%20and%20webhooks%20you%20can%20make%20public.%0A-%20A%20Hello%20webhook%2C%20a%20five-minute%20Heartbeat%2C%20and%20ready-to-use%20AI%20credentials.%0A-%20Nightly%20backups%2C%20including%20the%20key%20that%20unlocks%20your%20saved%20credentials.%0A-%20A%20backup%20restored%20and%20checked%20before%20you%20start.%0A%0ARead%20the%20whole%20recipe%2C%20including%20the%20closed%20details.%20For%20initial%20setup%2C%0Afollow%20%22Install%20by%20hand%22%20first%3A%20containers%2C%20example%20workflows%2C%20AI%0Acredentials%2C%20backup%20timer%2C%20and%20scratch%20restore%20check.%20The%20opening%0Awalkthrough%20is%20for%20the%20owner%20afterwards.%20Do%20not%20run%20the%20later%20Restore%0Aor%20Update%20instructions%20during%20initial%20setup.%0AWork%20as%20exedev%20in%20~%2Fn8n.%20Never%20overwrite%20an%20existing%20~%2Fn8n%2F.env%3A%20it%0Aholds%20the%20encryption%20key.%20Check%20it%20matches%20n8n-data%2Fconfig.%0ADo%20not%20create%20the%20owner%20account%3B%20the%20owner%20does%20that%20in%20the%20browser.%0ALeave%20public%20sharing%20off.%20Verify%20the%20Hello%20webhook%20at%20localhost%3A8000%0Aand%20confirm%20%2F%20and%20%2Frest%2Fsettings%20return%20404%20there.%20Wait%20up%20to%20five%0Aminutes%20for%20a%20successful%20Heartbeat%20execution.%20Check%20both%20AI%20credentials.%0AVerify%20docker.service%20is%20enabled%20and%20all%20containers%20use%20unless-stopped%3B%0Ado%20not%20reboot.%20Restore%20the%20backup%20only%20in%20the%20scratch%20directory%2C%20check%0Acredentials%20decrypt%2C%20then%20remove%20the%20scratch%20stack.%0AFinish%20with%20the%20private%20editor%20URL%20and%20the%20OPTIONAL%20command%20the%20owner%0Acan%20run%20on%20their%20own%20computer%20to%20allow%20public%20webhooks%3A%0Assh%20exe.dev%20share%20set-public%20my-n8n%0A%0AFinish%20with%20the%20URLs%20I%20should%20open.%0A to create a VM whose agent, Shelley, follows this recipe. From a terminal:

```sh
curl -fsSL https://hub.exe.dev/deploy/n8n/prompt.txt | ssh exe.dev new --prompt=/dev/stdin
```

Or follow the steps below by hand.

n8n connects your apps into little chains of “when this happens, do that.”
Send new leads to Slack, tidy up a spreadsheet, or summarize your inbox
with AI. Your VM keeps things ticking while your laptop naps.

## Open the editor

When Shelley finishes, open the editor URL it gives you. It'll look like
`https://my-n8n.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](https://docs.n8n.io/build-your-first-workflow)
or pick a [template](https://n8n.io/workflows/). Save, **Publish**, and off it goes.
Check the workflow's time zone before relying on a schedule; this setup
starts in UTC.

## 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:

```bash
ssh exe.dev share set-public my-n8n
curl -X POST https://my-n8n.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`.

## 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:

```bash
scp -r my-n8n.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.

Before an update, take a backup. Ask Shelley to help, or use the steps
below. Something stuck? On the VM, try
`cd ~/n8n && docker compose logs --tail=100`.

## The nuts and bolts

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

<details id="create-the-vm" data-manual-setup class="dp-details">
<summary>Install by hand</summary>

### Create a VM

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

```bash
ssh exe.dev new --name=my-n8n
ssh my-n8n.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:

```bash
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 stable 2.x release:

```bash
mkdir -p ~/n8n/n8n-data ~/n8n/postgres ~/n8n/workflows
cd ~/n8n
if [ ! -e .env ]; then
  N8N_VERSION=$(curl -fsSL https://registry.npmjs.org/-/package/n8n/dist-tags | jq -r .stable)
  case "$N8N_VERSION" in 2.*) ;; *) N8N_VERSION=2.43.5 ;; 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. Schedules default to UTC; change
`GENERIC_TIMEZONE` in `.env` if you'd like a different default.

Create the four-service Compose stack:

```bash
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
    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}
    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
```

exe.dev's [HTTPS proxy](/docs/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.

```bash
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](#troubleshooting).

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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.

```bash
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:

```bash
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:

```bash
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`:

```bash
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](/docs/integrations-llm):

```bash
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.

```bash
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.

```bash
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:

```bash
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:

```bash
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):

```bash
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](#open-the-editor).

</details>

<details id="back-up-and-restore" class="dp-details">
<summary>Back up and restore</summary>

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**. Copy backups off the VM as shown above.

### Restore

On a new VM, enable Docker and install `jq` as in
[Install](#install-n8n-with-postgresql). Create `~/n8n-backups` there, then
copy your backup over from your computer:

```bash
scp n8n-20261014-033012.tar.gz my-n8n.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:

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

Unpack the backup and set this VM's hostname:

```bash
mkdir ~/n8n
tar -xzf ~/n8n-backups/n8n-20261014-033012.tar.gz -C ~/n8n
cd ~/n8n
sed -i "s/^N8N_HOSTNAME=.*/N8N_HOSTNAME=$(hostname).exe.xyz/" .env
```

Load the database before starting n8n:

```bash
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.

</details>

<details id="update" class="dp-details">
<summary>Update n8n</summary>

Pick a stable 2.x version from n8n's
[release notes](https://docs.n8n.io/release-notes/). On the VM, back up,
then edit **only `N8N_VERSION`** in `.env`; both n8n and its runners use it:

```bash
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](#restore)—changing the image tag alone won't undo
database migrations. Read the upgrade guide before changing n8n or
PostgreSQL to a new major version.

</details>

<details id="troubleshooting" class="dp-details">
<summary>Something not working?</summary>

On the VM:

```bash
cd ~/n8n
docker compose logs --tail=100 n8n runners postgres webhooks
```

- **The main URL returns 404:** expected! Open the editor at
  `https://my-n8n.exe.xyz:5678/`; the main URL serves workflow endpoints only.
- **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.
- **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`.

</details>

## Frequently asked questions

### Is n8n free?

The Community edition is free to self-host. You pay for your VM; see [exe.dev plans](https://exe.dev/pricing). AI model usage is separate.

### Can I use it at work?

Yes, for internal business use. Offering n8n itself to customers is a different licensing question; check n8n's [Sustainable Use License](https://docs.n8n.io/n8n-community-license).

### 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](/docs/integrations-llm).

### Can I use my own domain?

Yes. Ask Shelley to connect your [custom domain](/docs/cnames) and update n8n's webhook address. The editor can stay private at its existing URL.


## Related

- [Self-host Uptime Kuma](https://hub.exe.dev/deploy/uptime-kuma.md): A little lookout for your websites. Green bars encouraged.
- [Run Claude Code on a remote server](https://hub.exe.dev/deploy/claude-code.md): A computer for Claude. A breather for your laptop.
- [Host OpenClaw](https://hub.exe.dev/deploy/openclaw.md): An AI sidekick with its own computer. Your laptop can take a nap.
- [VS Code in your browser, with code-server](https://hub.exe.dev/deploy/code-server.md): Your editor in a tab. Your projects right where you left them.
