惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
D
Darknet – Hacking Tools, Hacker News & Cyber Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
G
Google Developers Blog
MyScale Blog
MyScale Blog
Google DeepMind News
Google DeepMind News
J
Java Code Geeks
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
Check Point Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
P
Proofpoint News Feed
D
Docker
Jina AI
Jina AI
博客园 - 三生石上(FineUI控件)
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Help Net Security
Help Net Security
Google DeepMind News
Google DeepMind News
L
LINUX DO - 最新话题
T
Tailwind CSS Blog
N
Netflix TechBlog - Medium
Forbes - Security
Forbes - Security
MongoDB | Blog
MongoDB | Blog
Attack and Defense Labs
Attack and Defense Labs
Webroot Blog
Webroot Blog
A
About on SuperTechFans
Schneier on Security
Schneier on Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
Microsoft Azure Blog
Microsoft Azure Blog
F
Fortinet All Blogs
IT之家
IT之家
The Last Watchdog
The Last Watchdog
腾讯CDC
Microsoft Security Blog
Microsoft Security Blog
Project Zero
Project Zero
B
Blog
Recorded Future
Recorded Future
博客园_首页
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
SegmentFault 最新的问题
Security Archives - TechRepublic
Security Archives - TechRepublic
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
H
Hacker News: Front Page
T
Threatpost
H
Heimdal Security Blog
Cloudbric
Cloudbric
Google Online Security Blog
Google Online Security Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
云风的 BLOG
云风的 BLOG
V
Visual Studio Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
n8n self-hosted в production: docker-compose, nginx, ретраи и три грабли
viktdo · 2026-05-11 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение13 мин

Охват и читатели520

Туториал

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 через :2-latest вместо просто :latest:

image: n8nio/n8n:2-latest

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

Грабля номер один: 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. Бывало всё из того, что описано выше - и каждая грабля стоила нескольких часов отладки. Надеюсь, кому-то сэкономит время.