n8n self-hosted в production: docker-compose, nginx, ретраи и три грабли

n8n запускается одной командой docker run и через пять минут вы видите логин-форму. Это маркетинговый ролик. Реальный production-конфиг - с persistent storage, корректными webhook-URL, ретраями, бэкапами PostgreSQL и мониторингом - выглядит сильно иначе. В этой статье - конфигурация, которую я держу на 12 проектах в течение полутора лет. Плюс три грабли, на которые наступал лично.

Все примеры - community-edition, без коммерческой лицензии. На проде у меня сейчас крутится 2.19.5, но в image: стоит n8nio/n8n:latest плюс Watchtower (про него ниже) - он подтягивает свежий образ ночью. Внутри 2.x API/env-переменные стабильны, рекомендую :latest + Watchtower на проектах где простой 5 минут утром не критичен, и закреплённый минор (:2.19.5) - на проектах где даунтайм нельзя.

Полный production-стек

Я не пишу ручной nginx-конфиг. Не из лени, а потому что nginxproxy/nginx-proxy + nginxproxy/acme-companion делают то же самое сильно проще: новый контейнер с правильными VIRTUAL_HOST / LETSENCRYPT_HOST метками - сам подхватывается, сам получает сертификат, сам обновляется. Плюс Watchtower для авто-обновления образов ночью, Portainer для веб-GUI Docker, Redis для queue mode.

Маленькая историческая ремарка: если открываете старые туториалы и видите там jwilder/nginx-proxy и jrcs/letsencrypt-nginx-proxy-companion - это те же образы, проект просто переехал в namespace nginxproxy/* и теперь поддерживается ZeroSSL. Старые имена технически ещё работают (как и у меня в одном legacy-проекте), но активный maintain и свежие релизы там, куда я указал. На новой инсталляции берите nginxproxy/*.

Файл docker-compose.yml целиком (минимальный для статьи):

services:
  # ──────────── Реверс-прокси + HTTPS (auto-config через labels)
  proxy:
    image: nginxproxy/nginx-proxy:alpine
    container_name: nginx-proxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - nginx_certs:/etc/nginx/certs
      - nginx_vhost:/etc/nginx/vhost.d
      - nginx_html:/usr/share/nginx/html
    networks: [internal]

  letsencrypt:
    image: nginxproxy/acme-companion
    container_name: nginx-le
    restart: unless-stopped
    env_file: .env
    environment:
      - NGINX_PROXY_CONTAINER=nginx-proxy
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - nginx_certs:/etc/nginx/certs
      - nginx_vhost:/etc/nginx/vhost.d
      - nginx_html:/usr/share/nginx/html
    depends_on: [proxy]
    networks: [internal]

  # ──────────── PostgreSQL (доступен локально для SSH-туннеля)
  postgres:
    image: postgres:15-alpine
    container_name: n8n-postgres
    restart: unless-stopped
    env_file: .env
    environment:
      - POSTGRES_DB
      - POSTGRES_USER
      - POSTGRES_PASSWORD
    volumes:
      - pg_data:/var/lib/postgresql/data
    ports:
      - "127.0.0.1:5432:5432"
    networks: [internal]

  # ──────────── Redis (для queue mode)
  redis:
    image: redis:7-alpine
    container_name: n8n-redis
    restart: unless-stopped
    env_file: .env
    command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
    volumes:
      - redis_data:/data
    networks: [internal]

  # ──────────── n8n
  n8n:
    image: n8nio/n8n:latest
    container_name: n8n-app
    restart: unless-stopped
    env_file: .env
    environment:
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE
      - DB_POSTGRESDB_USER
      - DB_POSTGRESDB_PASSWORD
      - N8N_ENCRYPTION_KEY
      - N8N_DEFAULT_BINARY_DATA_MODE=filesystem
      - N8N_PROTOCOL=https
      - N8N_EDITOR_BASE_URL=https://${DOMAIN_N8N}/
      - WEBHOOK_URL=https://${DOMAIN_N8N}/
      - N8N_PROXY_HOPS=1
      - N8N_SECURE_COOKIE=false
      - VIRTUAL_HOST=${DOMAIN_N8N}
      - VIRTUAL_PORT=5678
      - CLIENT_MAX_BODY_SIZE=64m
      - LETSENCRYPT_HOST=${DOMAIN_N8N}
      - LETSENCRYPT_EMAIL=${LE_EMAIL}
      - GENERIC_TIMEZONE=Europe/Moscow
      - TZ=Europe/Moscow
      - NODE_FUNCTION_ALLOW_BUILTIN=crypto
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on: [postgres, proxy]
    networks: [internal]

  # ──────────── Portainer (веб-GUI Docker)
  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    restart: unless-stopped
    env_file: .env
    environment:
      - VIRTUAL_HOST=${DOMAIN_PORTAINER}
      - LETSENCRYPT_HOST=${DOMAIN_PORTAINER}
      - LETSENCRYPT_EMAIL=${LE_EMAIL}
      - VIRTUAL_PORT=9000
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data
    depends_on: [proxy, letsencrypt]
    networks: [internal]

  # ──────────── Watchtower (авто-обновления контейнеров)
  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    restart: unless-stopped
    command: --schedule "0 0 3 * * *" --cleanup
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks: [internal]

volumes:
  pg_data:
  redis_data:
  n8n_data:
  nginx_certs:
  nginx_vhost:
  nginx_html:
  portainer_data:

networks:
  internal:
    driver: bridge

И .env рядом:

# n8n
DOMAIN_N8N=n8n.example.ru
DOMAIN_PORTAINER=portainer.example.ru
LE_EMAIL=you@example.com
N8N_ENCRYPTION_KEY=сгенерируйте_32_символа_случайных

# Postgres
POSTGRES_DB=n8n
POSTGRES_USER=n8n
POSTGRES_PASSWORD=сгенерируйте_сильный_пароль

# Redis
REDIS_PASSWORD=сгенерируйте_сильный_пароль

Несколько моментов которые сильно не очевидны новичкам.

Никакого version: "3.8" в начале. Полностью устаревший атрибут, его выпилили из Compose Spec ещё в 2023-м. Docker Compose v2 на свежих машинах либо выдаёт жирный warning, либо отказывается стартовать (obsolete attribute). Просто не пишите эту строку, схему файла Compose определит сам. Если копируете чужие туториалы и видите version: в первой строке - удаляйте.

n8n никаких портов наружу не пробрасывает. Контейнер слушает :5678 внутри сети internal, наружу его пробрасывает только nginx-proxy через VIRTUAL_HOST метку. Это работает потому что nginx-proxy смонтирован к /var/run/docker.sock и сам определяет какие сервисы куда роутить. На свежей машине после docker compose up -d через пару минут на https://n8n.example.ru уже валидный TLS - companion сам пошёл в Let's Encrypt и взял сертификат.

Postgres 15-alpine. Тут оговорка: официальный репозиторий n8n-io/n8n-hosting сейчас в эталонных примерах использует postgres:16. На новой инсталляции имеет смысл брать именно 16. У меня в проде 15 не из принципа, а исторически: систему ставил полтора года назад, тогда 15 была свежей мажорной версией, всё работает, и pg_upgrade ради профита которого здесь нет - это даунтайм без выигрыша. n8n спокойно живёт на 13/14/15/16, под капотом TypeORM-схема без специфики мажора. Если у вас уже что-то стоит - не трогайте без необходимости. Если ставите с нуля - берите 16.

N8N_ENCRYPTION_KEY генерируется один раз и не меняется. Этим ключом n8n шифрует credentials в БД. Если поменяете - все ранее сохранённые токены/пароли в credentials превратятся в нечитаемый мусор, и придётся переподключать все интеграции руками. Сгенерируйте через openssl rand -hex 32 и сохраните в безопасное место. У меня хранится в 1Password плюс распечатан и лежит в офисе.

N8N_PROXY_HOPS=1 - n8n верит первому X-Forwarded-For заголовку для определения реального IP клиента (нужно для логов и rate-limit'ов). Если поставить больше - получите подмену IP через подделанные заголовки, если меньше - в логах увидите только IP nginx-proxy.

CLIENT_MAX_BODY_SIZE=64m - иначе тихий 413 на любом файле тяжелее 1 МБ. По умолчанию nginx режет тело запроса на 1 МБ. Webhook с PDF, фотографией или голосовухой больше этого размера получит 413 Request Entity Too Large от nginx до того, как до n8n вообще что-то долетит. Самое подлое - в n8n executions это видно как обрыв на webhook-узле без понятной причины: статус успешный (потому что nginx-проблема, не n8n), но binary.data пустой. Особенно больно ловит при работе с Telegram через getFile + загрузку контента (документы до 20 МБ, видео и голосовые - до 50 МБ). У nginxproxy/nginx-proxy лимит выставляется через env-переменную CLIENT_MAX_BODY_SIZE прямо на сервисе-backend'е - прокси сам подставит в vhost. Глобально на прокси тоже можно (та же переменная на контейнере proxy), но per-service гибче: статический сайт и webhook-инстанс редко требуют одинаковых лимитов.

Watchtower: зачем :latest это нормально (и почему критично для агентств)

Стандартный совет «всегда пинить минорную версию» в production - правильный для критичных систем где у вас есть инженер на постоянной поддержке. Но самый частый реальный сценарий self-hosted n8n в B2B - другой: студия/агентство развернуло инстанс под клиента, сдало его в эксплуатацию, и обслуживание после релиза или прекратилось, или ведётся фрагментарно по запросам. В таком сценарии стандартный совет ломается на ровном месте, и я объясню почему.

n8n - это публичный веб-интерфейс плюс runner кода. Регулярно (несколько раз в год) выходят критические security-обновления, закрывающие реальные уязвимости: SSRF через HTTP Request узлы, прокидывание credentials, prototype pollution в payload-парсерах, баги авторизации. История security advisories n8n на GitHub открытая, можете полистать.

Когда вы лично каждый день заходите в UI n8n под своим проектом - вы увидите верхнюю плашку «новая версия» сразу, как только она появится в Docker Hub, и при появлении в release notes слова Security оперативно её накатите. Когда тот же инстанс отдан клиенту, который в UI не заходит вообще, а доработки на нём не ведутся - этой плашки никто не увидит. Сертификаты пере-выпускаются автоматически, контейнер «работает», но внутри живёт незакрытая уязвимость, которая через полгода может стать чьим-то трофеем. Если у клиента n8n торчит наружу (а в 90% случаев да - туда же приходят webhook'и), это вопрос времени.

Watchtower эту дыру закрывает структурно: ночью в 03:00 он сам тянет свежий образ из Docker Hub, гасит и поднимает контейнер, всё. Никакой плашки не нужно - просто работает на той версии, что вышла последней. Стоимость - минутный даунтаут утром раз в несколько недель, который никто не заметит. Цена ущерба от не накатанной security-фиксы - на порядки выше.

Экономика для агентств, которые администрируют десяток инсталляций: n8n релизит 2-4 обновления в месяц. Это 20-40 рестартов в месяц на 10 проектов, если делать руками. По 10-15 минут на каждый (зайти, проверить changelog, рестартнуть, прогреть, убедиться что цепочки живые) - 5-10 часов в месяц просто на «не запустить уязвимый контейнер у клиента». Watchtower с расписанием --schedule "0 0 3 * * *" --cleanup это всё закрывает за ноль часов в месяц.

Логика конфигурации:

  1. n8n релизит обновления часто (2-4 раза в месяц), и в подавляющем большинстве это патчи интеграций, фиксы багов, плюс security

  2. Breaking changes в minor-релизах внутри одной major-серии (2.x) практически отсутствуют - конфиги, env-переменные, API стабильны

  3. Watchtower обновляет только если в Docker Hub появился новый image - не дёргает контейнер просто так

  4. --cleanup удаляет старые образы после успешного обновления - диск не забивается на 100 ГБ за полгода

За полтора года на десятке клиентских проектов Watchtower уронил систему один раз - при переходе с 1.x на 2.x была необходимость в ручной миграции. Это плата за ноль часов ручного обслуживания десятка инсталляций.

Если хочется подстраховаться от major-сюрприза, у n8n в Docker Hub есть три рабочих варианта. Floating-теги только глобальные (:latest:stable:next:beta:nightly), отдельных floating'ов под major-серию они не публикуют - значит закрепиться можно либо через конкретный релиз, либо через внешний контроль:

  1. :latest + Watchtower - моя текущая, принимаю риск что когда-нибудь так же прилетит 3.x как было с 1.x → 2.x

  2. Конкретный минор :2.20.6 без Watchtower - стабильно, но руками обновлять каждые 2-3 недели когда выходит новая минорная

  3. Кастомный daemon: раз в день дёргает Docker Hub Tags API, находит свежайший тег по regexp ^2\.\d+\.\d+$, и если он новее текущего - переписывает compose и рестартит. Аккуратно, но 20 строк bash/python и собственный мониторинг на падения этого скрипта

Грубая прикидка соотношения «затрат на ручное обслуживание ÷ риск пропустить security» для агентских проектов: раз в полгода поднять упавший после автоапдейта workflow на одном проекте дешевле, чем 5-10 часов в месяц ручных обновлений десятка контейнеров, плюс риск, что в одном из них тихо живёт CVE, который мы не накатили потому что в UI к клиенту никто не заходит. Поэтому держу схему №1.

Грабля номер один: WEBHOOK_URL

Самая распространённая ошибка новичков - webhook-узел сгенерировал URL вида http://localhost:5678/webhook/abc123, и человек тыкает его в ручку API. Понятно, что не работает.

Корень проблемы: переменная WEBHOOK_URL в env. Если её не задать или задать неверно, n8n использует значение по умолчанию (на основе N8N_HOST). У меня были случаи, когда сервер слушал 0.0.0.0WEBHOOK_URL не был задан, и весь production окей дёргал HTTP-эндпоинт без TLS - пока однажды партнёрский сервис не перешёл на строгую SSL-проверку и всё легло.

Проверка после деплоя:

curl -s https://n8n.example.ru/healthz
# {"status":"ok"}

И в самом интерфейсе создайте тестовый Webhook-узел, посмотрите URL который он показывает в правой панели. Если там http://localhost:5678/... - WEBHOOK_URL не подхватился, рестарт контейнера обязателен.

Отдельный случай: N8N_EDITOR_BASE_URL и WEBHOOK_URL могут быть разными доменами, и это не баг, а фича. У меня в проде так:

N8N_EDITOR_BASE_URL=https://n8n.example.ru/
WEBHOOK_URL=https://tg.example.ru/

Это нужно когда webhook-эндпоинты выставлены через отдельный CDN/туннель (про cloudflared дальше будет отдельная глава, там как раз про этот случай).

Бэкапы PostgreSQL

n8n хранит всю историю выполнений и конфигурацию workflow в PostgreSQL. Потеря БД - потеря всего, что вы настраивали месяцами. Бэкап через pg_dump в crontab:

# /etc/cron.d/n8n-backup
0 3 * * * root docker exec -t n8n-postgres pg_dumpall -c -U n8n | gzip > /var/backups/n8n/n8n-$(date +\%F).sql.gz
0 4 * * 0 root find /var/backups/n8n/ -name "*.sql.gz" -mtime +30 -delete

Каждое утро в 03:00 - полный дамп, в 04:00 в воскресенье - чистка старых файлов (хранится месяц). Дамп жмётся в gzip, занимает порядка 5-10 МБ на 200 активных workflow.

Восстановление:

gunzip < /var/backups/n8n/n8n-2026-05-08.sql.gz | docker exec -i n8n-postgres psql -U n8n

Делал три раза за полтора года - всегда отрабатывало. Один раз потеряли неделю работ из-за того, что бэкап делался, но не копировался на внешний сервер. Мораль: бэкапы должны лежать минимум в двух местах. У меня сейчас локальный + еженочный rsync на S3-совместимое хранилище у Beget'а.

Грабля номер два: очередь выполнений и память

n8n по умолчанию хранит все executions в БД. На активных workflow таблица execution_entity растёт быстро - у одного клиента она достигла 18 ГБ за 4 месяца, n8n начал тормозить и валиться по OOM. Решение в env:

EXECUTIONS_DATA_PRUNE: "true"
EXECUTIONS_DATA_MAX_AGE: 168       # часов = 7 дней
EXECUTIONS_DATA_PRUNE_MAX_COUNT: 10000

После включения n8n чистит данные старше 7 дней, лимитирует общее число до 10000. На моём проде таблица стабилизировалась на 1.2 ГБ.

Дополнительный момент: если у вас много параллельных workflow с тяжёлой логикой, переходите на режим очередей с Redis:

EXECUTIONS_MODE: queue
QUEUE_BULL_REDIS_HOST: redis
QUEUE_BULL_REDIS_PORT: 6379

И добавляете worker-сервисы в docker-compose. Без queue mode параллельный лимит ограничен N8N_CONCURRENCY_PRODUCTION_LIMIT (по умолчанию -1 = без лимита, но всё в одном Node-процессе - на пиках падает).

Грабля номер три: webhook-задержки на холодном старте

После рестарта контейнера первый webhook-вызов может ждать ответа 5-10 секунд - n8n инициализирует runtime, читает workflow из БД, прогревает кеш узлов. Если ваш партнёрский сервис ставит таймаут 5 секунд (это многие платёжки) - он считает webhook неуспешным и иногда повторяет запрос.

Что важно знать с Watchtower: контейнер обновляется ночью в 03:00, дальше до первого webhook-запроса проходит несколько часов (бизнес-партнёры просыпаются). Первый утренний запрос неизбежно холодный и медленный. На критичных проектах я после рестарта явно прогреваю n8n самостоятельно.

Решение: warm-up-скрипт, который дёргает healthcheck-эндпоинт сразу после старта:

#!/bin/bash
docker compose up -d
sleep 5
for i in {1..10}; do
  curl -fs https://n8n.example.ru/healthz && break
  sleep 2
done
echo "n8n ready"

Альтернативно - на стороне webhook-источника поставить ретрай с экспоненциальным backoff, если у партнёра это возможно. Ещё один вариант (если у вас Watchtower) - повесить cron на 03:05 на сервере с этим warm-up-скриптом сразу после ночного апдейта. Тогда даже первый утренний запрос будет от уже разогретого n8n.

Российская специфика: cloudflared для Telegram webhook

Не очевидный для большинства туториалов момент. Telegram Bot API не принимает webhook'и на IP-адреса российских хостингов - после серии политических событий и обновлений списков. Это значит что прямой setWebhook на n8n.example.ru где example.ru указывает на IP вашего РФ-VPS - не сработает. TG-API ответит {"ok":false,"error_code":400,"description":"Bad Request: bad webhook"} либо «успешно» зарегистрирует, но события приходить не будут.

Решение - Cloudflare Tunnel. Контейнер cloudflared устанавливает исходящее соединение к CF Edge, и Telegram бьёт в CF (не имеющий привязки к РФ-IP), а CF проксирует через Tunnel внутрь вашего n8n. С точки зрения TG webhook лежит на cloudflare-домене:

# добавить в docker-compose.yml
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    command: tunnel --no-autoupdate run
    environment:
      - TUNNEL_TOKEN=${CLOUDFLARED_TOKEN}
    networks: [internal]
    depends_on: [n8n]

Tunnel token берётся в CF Dashboard: Zero Trust → Networks → Tunnels → Create. Внутри туннеля настраиваете один public hostname (например tg.example.ru или hooks.example.ru) и роутите его на http://n8n:5678. Сертификат CF выдаёт сам, ничего настраивать не нужно.

В env при этом:

N8N_EDITOR_BASE_URL=https://n8n.example.ru/   # прямой через nginx-proxy
WEBHOOK_URL=https://tg.example.ru/             # через CF Tunnel

То есть UI работает по прямой ссылке без CF (быстрее), а webhook-эндпоинты в Telegram-нодах получают URL через туннель. Не-Telegram интеграции (Tilda, AmoCRM, CRM) при этом продолжают принимать запросы по основному домену, потому что n8n слушает webhook независимо от того по какому host header пришёл запрос.

Стоимость - бесплатно для нашего use-case (просто туннелирование без CF Access авторизации). Cloudflare явных публичных лимитов на bandwidth/RPS для free tier не объявляет; на webhook-нагрузках за пол года я никаких ограничений не встречал, даже на проекте с пиками 100+ TG-сообщений в минуту. Latency Tunnel'а добавляет к webhook'у +50-100 мс, в нашем случае это незаметно. Альтернатива - VPS за границей с проксированием на РФ - дороже, сложнее, чаще обрывается.

Ретраи внутри workflow

Стандартный узел HTTP Request не делает ретраи автоматически. Если внешний API ответил 502 - workflow упадёт, и без обработки ошибок это приведёт к потере данных.

Минимальная обёртка через узел Error Trigger или через настройки самого узла:

HTTP Request settings:
  Retry On Fail: ON
  Max Tries: 3
  Wait Between Tries: 5000   # ms

Этого достаточно для 80% случаев. Для критичных операций (платежи, отправка SMS) добавляю отдельный path через узел If:

[HTTP Request] → [If: status >= 500]
                   ↓ true
                 [Wait 30s] → [HTTP Request retry] → [Postgres: log]
                   ↓ false
                 [Continue normal flow]

Логирование в Postgres даёт возможность поднять историю фейлов и расследовать проблемы постфактум. У меня в сервисной таблице:

CREATE TABLE n8n_failures (
    id BIGSERIAL PRIMARY KEY,
    workflow_id TEXT NOT NULL,
    node_name TEXT NOT NULL,
    error_text TEXT,
    payload JSONB,
    occurred_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX ON n8n_failures (occurred_at DESC);
CREATE INDEX ON n8n_failures (workflow_id);

Раз в неделю прогоняю агрегацию по workflow_id, node_name - вижу узлы с топ-ошибками и фикшу.

Мониторинг

n8n с N8N_METRICS: true отдаёт Prometheus-эндпоинт на /metrics. Минимальный stack - Prometheus + Grafana + Alertmanager:

# docker-compose.monitoring.yml
prometheus:
  image: prom/prometheus
  volumes:
    - ./prometheus.yml:/etc/prometheus/prometheus.yml
  ports:
    - "127.0.0.1:9090:9090"

grafana:
  image: grafana/grafana
  ports:
    - "127.0.0.1:3000:3000"
  environment:
    GF_AUTH_ANONYMOUS_ENABLED: "true"

В prometheus.yml:

scrape_configs:
  - job_name: n8n
    static_configs:
      - targets: ["n8n:5678"]
    metrics_path: /metrics

Ключевые метрики, по которым стоит ставить алерты:

  • n8n_active_workflows - если упало до 0, что-то сломалось

  • n8n_workflow_failed_total - рост говорит о проблеме с интеграцией

  • Высокий n8n_node_running_time_seconds на конкретных узлах - узкое место

На простых инсталляциях вместо Prometheus достаточно uptime-кобота, который дёргает /healthz каждые 60 секунд и шлёт в Telegram при недоступности.

Когда self-hosted не нужен

После всего написанного честный итог: если у вас 10-30 простых workflow в месяц, вы платите за VPS 700 рублей, и нет команды на DevOps - берите cloud n8n.io, тариф Starter за 20 евро в месяц. Получите managed-сервис с автообновлениями, бэкапами и поддержкой. На 30-100 операций в день экономия времени окупает разницу в цене.

Self-hosted имеет смысл, когда:

  1. Объём операций превышает 1000 в день - расходы на cloud начинают расти быстрее, чем VPS

  2. Есть требование 152-ФЗ - данные клиентов должны физически быть в РФ

  3. У вас есть инженер на 1-2 часа в неделю на поддержку

  4. Нужна интеграция с внутренней инфраструктурой (private API, базы данных за корпоративным VPN)

Иначе - пользуйтесь готовым cloud, не разводите серверный зоопарк.

Полезные ссылки

Если что-то сломается, что не описано здесь, - смотрите логи контейнера через docker logs n8n -f --tail=200. В 90% случаев причина видна сразу: либо упала PostgreSQL (нет места на диске, рост таблицы executions), либо webhook не доходит из-за неверного WEBHOOK_URL, либо timeouted внешний API (увеличить proxy_read_timeout в nginx).

Эта конфигурация обкатана на проде в одной студии чат-ботов, делающей в среднем 50-100к операций в месяц на n8n. Бывало всё из того, что описано выше - и каждая грабля стоила нескольких часов отладки. Надеюсь, кому-то сэкономит время.

Комментарии (6)


  1. Emulyator
    11.05.2026 10:44

    Не однократно встречал мнение, что n8n теряет привлекательность как простой и удобный в освоении и применении инструмент на фоне альтернативы в виде "вайбкодинга" того же саомго. По крайней мере для тех кто имеет хотя бы базовые навыки программирования. Какое у n8n преимущество, если все равно придется разбираться со сложной для новичков настройкой системы на реальных проектах?


    1. viktdo Автор
      11.05.2026 10:44

      Вопрос действительно становится острее каждый год. Не буду защищать n8n как «универсальный инструмент» - для разработчика с навыками вайбкоднуть интеграцию в Python/Node часто быстрее. Но есть несколько ниш где n8n остаётся выбором даже у тех кто умеет кодить.

      Передача в эксплуатацию клиенту. Самое важное. Vibe-coded решение клиент не поправит сам - изменить шаблон сообщения, добавить шаг в воронку, поменять условие. В n8n его маркетолог или секретарь зайдёт в UI и сделает. Я лично 70% клиентских проектов веду в n8n именно по этой причине: после релиза приходит правка вида «добавьте ещё одно поле в форме», и я хочу чтобы клиент мог либо сам, либо за 15 минут чужими руками без меня.

      Визуальный дебаг на пересечении 5+ систем. Когда в pipeline CRM → AI-классификация → Postgres → отправка в TG → запись результата - видеть всю цепочку с реальными данными на каждом шаге сильно быстрее чем читать логи. На простой интеграции из 2-3 точек разница почти ноль, на 8-10 точках n8n заметно быстрее.

      150+ готовых интеграций с авторизацией. Если проект про «загнать данные из AmoCRM в Google Sheets и пнуть Telegram» - в n8n это 4 ноды без единой строки кода. Vibe-coded решение того же - 200 строк с OAuth обвязкой, retry, rate-limit handlers, которые ещё надо тестировать.

      Где n8n объективно проигрывает вайбкодингу, соглашусь:

      • Сложная бизнес-логика с ветвлениями - в коде читается лучше чем canvas из 50 нод

      • Юнит-тесты - в n8n их по сути нет (есть pin data, но это не pytest)

      • Высокие нагрузки - Node.js под капотом, для 500+ RPS уже жмёт

      • Командная разработка с git-flow - workflow это JSON в БД, диффы и мерджи не самые удобные

      Лично у меня правило простое: 3-5 интеграций, один разработчик который сам поддерживает - вайбкодинг быстрее. 20-30 workflow в проде, часть проектов отдаётся клиентам в эксплуатацию, и в команде есть не-разработчики которые правят логику - n8n выигрывает по совокупности.


  1. onvova
    11.05.2026 10:44

    После этого я закрепил major через :2-latest вместо просто :latest:

    image: n8nio/n8n:2-latest

    странно, что оно работает:

     docker pull n8nio/n8n:2-latest
    Error response from daemon: manifest for n8nio/n8n:2-latest not found: manifest unknown: manifest unknown


  1. AGmind
    11.05.2026 10:44

    Спасибо, полезно. От себя добавлю плюс n8n, который часто недооценивают: он одинаково хорош и как главный оркестратор, и как одна нода внутри другого. У нас, например, RAG крутится на Dify, а n8n там дёргается HTTP-нодой на cron, вебхуки и склейку API. От задачи зависит — в обе стороны нормально.

    Ну и WEBHOOK_URL=localhost:5678 — классика, все через неё проходили :)


    1. viktdo Автор
      11.05.2026 10:44

      Хороший угол, согласен. У нас бывает наоборот - n8n как главный оркестратор, а HTTP-нодами подключаются специализированные сервисы: для embedding’ов YandexGPT, для re-rank’а отдельный микросервис на FastAPI, для voice-stream’а свой WS-relay. Идея та же - брать каждый инструмент за то в чём он силён, а не пытаться весь pipeline собрать в одном.

      Про Dify конкретно интересно - как у вас стык реализован: HTTP-нода в Dify зовёт webhook n8n или наоборот, n8n триггерит Dify через их Workflow API? И не ловите latency-проблем на длинных цепочках через два оркестратора сразу?

      «WEBHOOK_URL=localhost:5678» это правда классика, через неё каждый второй self-hosted-щик проходит в первый месяц :) У нас в onboarding-чек-листе буквально первая строка «проверь что webhook-нода возвращает URL по твоему домену, а не localhost».


  1. Mersavets
    11.05.2026 10:44

    После названия конфига docker-compose.yml можно не читать