В предыдущей статье мы рассмотрели логику работы проекта, познакомились с системными и аппаратными требованиями, узнали о настройках DNS, создали Telegram-бота и научились получать и правильно сохранять API-токен и Chat ID.
Теперь настало время углубиться в технические детали реализации проекта, протестировать его и выпустить в продакшн. В этой статье вас ждет следующее:
- Разбор стека, свойств и настроек сервисов.
- Запуск стека на VPS.
- Детальный анализ workflow n8n.
- Запуск и тестирование workflow n8n в продакшен.
- Выводы.
Цикл статей построен так, что попробовать запустить проект на собственных мощностях и модернизировать его под индивидуальные задачи можно сразу. Достаточно, чтобы был подходящий по конфигурации VPS с установленным на нем Docker. Исходники проекта лежат в двух репозиториях: compose-файл для установки и запуска стека находится здесь, а отсюда можно скачать workflow для его запуска в n8n.
Стек
Бэкенд нашего проекта построен на микросервисной архитектуре: каждый сервис отвечает за свою задачу, а обращаться к публичным сервисам можно через адресную строку в браузере. Схематично взаимосвязь микросервисов можно представить следующим образом:
В нашем стеке n8n — это корневой сервис, который выполняет основную работу: принимает сообщения от пользователя через Telegram, передает инструкции в LLM и сохраняет заметку в Obsidian с помощью API. Другие сервисы — вспомогательные. Отдельно стоит отметить Caddy — обратный прокси-сервер, который обеспечивает TLS-соединение между микросервисами и предоставляет HTTPS-доступ к домену и поддоменам.
Состав стека и свойства сервисов
Caddy — обратный прокси-сервер с автоматически выпускаемыми TLS-сертификатами. Фишка Caddy в том, что он сам выпускает сертификаты Let’s Encrypt и сам следит за их автообновлением. Мы используем это свойство для автоматического выпуска TLS-сертификатов для n8n и Obsidian.
Сервис Caddy в составе compose-файла имеет следующие свойства:
image: caddy:latest # Образ Caddy (внутренний CA, автосертификаты, проксирование)
container_name: caddy # Фиксированное имя контейнера (удобно для docker exec/logs)
restart: unless-stopped # Автоперезапуск контейнера, пока он не будет остановлен в ручном режиме
ports:
- "${CADDY_HTTP_HOST_PORT}:${CADDY_HTTP_CONTAINER_PORT}" # HTTP с хоста -> Caddy (обычно 80:80)
- "${CADDY_HTTPS_HOST_PORT}:${CADDY_HTTPS_CONTAINER_PORT}" # HTTPS с хоста -> Caddy (обычно 443:443)
env_file:
- .env # Подгружаем переменные из файла .env для шаблонов и настроек
volumes:
- caddy_data:/data # Данные Caddy (в т.ч. PKI/сертификаты) — общий том для n8n
- caddy_config:/config # Служебная конфигурация Caddy
- html_notes:/html_notes:ro # Статика (HTML) из mkdocs — только чтение
- ./Caddyfile.template:/etc/caddy/Caddyfile.template:ro # Шаблон Caddyfile (с ${...}) — только чтение
# Команда запуска Caddy делает 3 вещи:
# 1) Ставит `gettext` (в нем есть `envsubst`) -- если пакет уже есть, продолжаем без ошибки.
# 2) Генерирует итоговый `/etc/caddy/Caddyfile` из шаблона `/etc/caddy/Caddyfile.template`, подставляя переменные окружения из `.env` (через `env_file`).
# 3) Стартует Caddy с получившимся конфигом.
# Важно: комментарии нельзя вставлять внутрь строки /bin/sh -c "..." - это ломает команду.
command: >
/bin/sh -c "
apk add --no-cache gettext >/dev/null 2>&1 || true;
envsubst < /etc/caddy/Caddyfile.template > /etc/caddy/Caddyfile &&
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
"
networks:
- ollama-network # Общая сеть для проксируемых сервисов
depends_on:
- mkdocs # Ждем контейнер mkdocs, чтобы статика была доступна
caddy-perms-fixer — сервис, который автоматически (каждые 300 секунд) выставляет нужные права на чтение и владельца для TLS-сертификатов Caddy, чтобы n8n мог их прочитать. Сервис в compose-файле выглядит следующим образом:
image: alpine:latest # Легкий образ для запуска shell-скрипта
container_name: caddy-perms-fixer # Фиксированное имя контейнера
restart: unless-stopped # Автоперезапуск контейнера, пока он не будет остановлен в ручном режиме
volumes:
- caddy_data:/data # Монтируем тот же том, где Caddy хранит /data/caddy/pki/...
- ./fix-caddy-pki-perms.sh:/usr/local/bin/fix-caddy-pki-perms.sh:ro # Скрипт коррекции прав — только чтение
command: ["/bin/sh", "-c", "while true; do /usr/local/bin/fix-caddy-pki-perms.sh; sleep 300; done"] # Переодически приводит права к читаемым
networks:
- ollama-network # Сеть не обязательна, но унифицируем
ollama — фреймворк для запуска LLM локально как на GPU, так и на CPU. Модели скачиваются отдельно через Open WebUI и хранятся в томе ollama_data:/root/.ollama.
Сервис в стеке представлен так:
image: ollama/ollama:latest # Используется последняя версия Ollama
container_name: ollama # Имя контейнера (на него ссылаются другие сервисы по DNS в сети)
restart: unless-stopped # Автоперезапуск
volumes:
- ollama_data:/root/.ollama # Хранилище моделей/кэша Ollama (переживает перезапуски)
networks:
- ollama-network # Сеть, чтобы Open WebUI и n8n ходили на http://ollama:...
environment:
- OLLAMA_NUM_PARALLEL=${OLLAMA_NUM_PARALLEL} # Параллельные запросы/генерации (из .env)
- OLLAMA_MAX_LOADED_MODELS=${OLLAMA_MAX_LOADED_MODELS} # Сколько моделей держать загруженными (из .env)
- OLLAMA_MODELS=/root/.ollama/models # Путь каталога моделей внутри контейнера
dns:
- 1.1.1.1 # Явно задаём DNS (Cloudflare) так как в образе по умолчанию DNS не задан
- 1.0.0.1 # Второй DNS
open-webui — открытый web UI для общения с Ollama. Сервис не имеет специальных настроек и представлен в стеке следующим образом:
image: ghcr.io/open-webui/open-webui:main # Образ Open WebUI
container_name: open-webui # Имя контейнера
restart: unless-stopped # Автоперезапуск
volumes:
- open-webui_data:/app/backend/data # Данные WebUI (пользователи/настройки/история)
depends_on:
- ollama # Ожидание запуска контейнера с Ollama
environment:
- OLLAMA_BASE_URL=http://ollama:${OLLAMA_CONTAINER_PORT} # URL Ollama внутри docker-сети (порт из .env)
networks:
- ollama-network # Общая сеть
extra_hosts:
- host.docker.internal:host-gateway # Доступ из контейнера к хосту по имени host.docker.internal
n8n — low-code, node-based фреймворк для автоматизации процессов. n8n имеет множество настроек через переменные окружения, самые важные из них: WEBHOOK_URL — базовый URL для вебхуков, необходимый для интеграции с Telegram; NODE_EXTRA_CA_CERTS — путь к TLS-сертификатам, которым должен доверять n8n. Чтобы n8n видел TLS-сертификаты по пути, установленному переменной окружения NODE_EXTRA_CA_CERTS, нужно примонтировать том caddy_data:/caddy_data:ro в режиме только для чтения.
Сервис в compose-файле выглядит следующим образом:
image: n8nio/n8n:${N8N_VERSION} # Версия n8n фиксируется через .env
container_name: n8n # Имя контейнера
restart: unless-stopped # Автоперезапуск
volumes:
- n8n_data:/home/node/.n8n # Персистентные данные n8n (учётные записи и workflows)
- caddy_data:/caddy_data:ro # Read‑only доступ к CA Caddy (root.crt) через общий том
depends_on:
- ollama # Ожидание старта контейнера с Ollama
environment:
- N8N_HOST=n8n.${DOMAIN} # Публичный хост UI n8n (используется для ссылок/redirect)
- N8N_PROTOCOL=https # Протокол снаружи (через Caddy TLS)
- N8N_PORT=${N8N_CONTAINER_PORT} # Порт, который слушает n8n внутри контейнера (подтягивается из .env)
- WEBHOOK_URL=https://${DOMAIN}/ # Базовый URL для вебхуков (важно для внешних интеграций)
- N8N_EDITOR_BASE_URL=https://${DOMAIN}/ # База URL редактора (чтобы UI генерил правильные ссылки)
- N8N_SECURE_COOKIE=${N8N_SECURE_COOKIE} # Secure-cookie для HTTPS (true/false из .env)
- NODE_EXTRA_CA_CERTS=/caddy_data/caddy/pki/authorities/local/root.crt # Доверять самоподписанному CA Caddy (TLS)
networks:
- ollama-network # Общая сеть
netdata — сервис мониторинга потребления ресурсов VPS, аналог Grafana, но с более дружелюбным интерфейсом и готов к использованию из коробки. Настроек у сервиса очень много, но для нашего проекта критически важных среди них нет.
В стеке сервис выглядит следующим образом:
image: netdata/netdata:latest # Последняя версия образа Netdata
container_name: netdata # Имя контейнера
hostname: ${NETDATA_HOSTNAME} # Отображаемое имя хоста в UI Netdata (из .env)
restart: unless-stopped # Автоперезапуск
cap_add:
- SYS_PTRACE # Нужно для части системных метрик/профилирования
- SYS_ADMIN # Нужно для части системных метрик (высокие привилегии)
security_opt:
- apparmor:unconfined # Убираем ограничения AppArmor, иначе часть метрик может не работать
volumes:
- netdataconfig:/etc/netdata # Конфиги Netdata
- netdatalib:/var/lib/netdata # База/состояние Netdata
- netdatacache:/var/cache/netdata # Кэш Netdata
- /etc/passwd:/host/etc/passwd:ro # Для маппинга UID->имена пользователей # Для маппинга UID->имена пользователей
- /etc/group:/host/etc/group:ro # Для маппинга GID->группы # Для маппинга GID->группы
- /etc/localtime:/etc/localtime:ro # Корректное время/таймзона в контейнере
- /proc:/host/proc:ro # Метрики процессов/ядра
- /sys:/host/sys:ro # Метрики системы/устройств
- /etc/os-release:/host/etc/os-release:ro # Информация об ОС
- /var/log:/host/var/log:ro # (Опционально) чтение логов хоста для интеграций
- /var/run/docker.sock:/var/run/docker.sock:ro # Доступ к Docker API для метрик контейнеров
networks:
- ollama-network # Общая сеть (если проксируете Netdata через Caddy)
environment:
- NETDATA_DISABLE_CLOUD=1 # Отключаем Netdata Cloud (не шлём метрики наружу)
obsidian — Markdown-система заметок с раздельными хранилищами и graph-представлением связей между заметками. Obsidian — простой сервис, специальных или сложных настроек у него нет. Сервис в compose-файле выглядит следующим образом:
image: lscr.io/linuxserver/obsidian:latest # LinuxServer.io образ Obsidian
container_name: obsidian # Имя контейнера
restart: unless-stopped # Автоперезапуск
security_opt:
- seccomp:unconfined # Снимаем seccomp-ограничения (иногда нужно для GUI/Chromium внутри)
environment:
- PUID=${PUID:-1000} # UID владельца файлов в примонтированных томах (по умолчанию 1000)
- PGID=${PGID:-1000} # GID владельца файлов (по умолчанию 1000)
- TZ=${TZ:-Europe/Moscow} # Таймзона контейнера
- CUSTOM_USER=${OBSIDIAN_USER:-admin} # Логин для доступа к UI (из .env)
- PASSWORD=${OBSIDIAN_PASSWORD:-admin} # Пароль для доступа к UI (из .env)
- NO_DECOR=true # Убрать декорации окна (косметика UI)
- NO_FULL=true # Не разворачивать на весь экран по умолчанию
- DISABLE_IPV6=true # Отключить IPv6 внутри контейнера
- LC_ALL=en_US.UTF-8 # Локаль (чтобы корректно работали шрифты/кодировки)
volumes:
- obsidian_config:/config # Конфиги и состояние приложения
- obsidian_vaults:/config/vaults # Vault'ы Obsidian (используются также mkdocs)
ports:
- "${OBSIDIAN_HOST_PORT}:${OBSIDIAN_CONTAINER_PORT}" # Порт UI Obsidian на хосте -> в контейнер
- "${OBSIDIAN_API_PORT_HOST}:${OBSIDIAN_API_PORT_CONTAINER}" # (Если нужно) API/служебный порт
devices:
- /dev/dri:/dev/dri # Проброс GPU (ускорение рендера/кодеков)
shm_size: "1gb" # Shared memory для браузера/GUI (уменьшает вылеты от нехватки /dev/shm)
networks:
- ollama-network # Общая сеть
article-parser — самописный Python-сервис парсинга текста статей по URL с очисткой от HTML-кода. Назначение сервиса — подготовка текстов статей для прочтения LLM. Если в LLM отправить просто ссылку на статью, то LLM откроет статью как web-страницу с большим количеством ненужного HTML-кода. Сервис удаляет все элементы страницы, кроме самого текста статьи.
Такой подход экономит ресурсы LLM и время на обработку текста. У сервиса нет специальных настроек, а в составе compose-файла он представлен следующим образом:
image: article_parser:latest # Локально собранный образ (смотрите /root/articles_parser)
container_name: article-parser # Имя контейнера
restart: unless-stopped # Автоперезапуск
expose:
- "${ARTICLE_PARSER_PORTS}" # Открываем порт только внутри сети (не публикуем на хост)
networks:
- ollama-network # Общая сеть
MkDocs — система сборки документации из Markdown-файлов на Python. В нашем проекте MkDocs автоматически конвертирует заметки, сделанные в Obsidian, в HTML-страницы с удобным доступом по URL. Сам же Obsidian в бесплатной версии не предоставляет подобной возможности. Поэтому у нас подключен том Obsidian к MkDocs: obsidian_vaults:/obsidian_vaults:ro.
Сервис в нашем стеке выглядит так:
image: squidfunk/mkdocs-material:latest # Последняя версия образа MkDocs Material
container_name: mkdocs # Имя контейнера
restart: unless-stopped # Автоперезапуск
volumes:
- obsidian_vaults:/obsidian_vaults:ro # Источник: vault'ы Obsidian — только чтение
- html_notes:/html_output # Выход: собранный HTML (его отдаёт Caddy)
- ./mkdocs:/docs # Конфиг/скрипты mkdocs (mkdocs.yml, entrypoint.sh и т.д.)
environment:
- OBSIDIAN_VAULTS_PATH=/obsidian_vaults # Путь к vault'ам внутри контейнера
- MKDOCS_DOCS_PATH=/docs/docs # Путь к исходным markdown внутри /docs
- MKDOCS_CONFIG_PATH=/docs/mkdocs.yml # Путь к конфигу MkDocs
entrypoint: /docs/entrypoint.sh # Кастомный entrypoint: генерация/сборка сайта
networks:
- ollama-network # Общая сеть
depends_on:
- obsidian # Ждём vault'ы/контейнер (хотя том и так общий)
Всего в стеке 9 микросервисов, связанных сетью и защищенных TLS-сертификатами. Сам стек без запущенных моделей потребляет около 2 GB оперативной памяти и порядка 50 GB жесткого диска.
Запуск стека на VPS
Зайдите на VPS и выполните команду git clone https://github.com/Pseudolukian/docker_composes/, затем перейдите в директорию ollama_openwebui_n8n и выполните команду docker compose up -d. Если Docker Compose у вас еще не установлен — обратитесь к нашему разделу базы знаний про Docker, где есть подробные инструкции по установке Docker и описаны Docker-команды, нужные для запуска стека.
Проверить статус контейнеров можно с помощью команды docker compose ps --status running --format "table {{.Name}}\t{{.State}}" | head. Статус (STATE) всех контейнеров должен быть running:
Если какой-то из контейнеров не запустился — понять причину ошибки поможет команда docker logs
Разбор n8n workflow
Ключевыми узлами работы нашего workflow являются Telegram-, HTTP Request- и IF-ноды, а сердцем и мозгом — Basic LLM Chain-нода. Графически workflow выглядит следующим образом:
Скачать workflow проекта в JSON-формате можно из этого репозитория или просто указать этот URL в пустом workflow для импорта.
Разберем свойства нод, которые используются в workflow, и сразу покажем, как они работают в нашем проекте.
HTTP Request-нода
С помощью HTTP Request-ноды n8n связывается по API с Obsidian (сохранение заметки, обновление TOC, проверка операции сохранения заметки) и с Python-сервисом article-parser (отправка ссылки от пользователя для очистки текста от HTML-тегов). HTTP Request-нода находится в основном разделе выбора нод:
Для работы ноды нужно обязательно заполнить следующие поля:
- Method — тип запроса, который принимает эндпоинт: GET (получение данных), POST (отправка данных, обычно с телом запроса), PUT (создание или полная замена ресурса), DELETE (удаление ресурса), HEAD (получение заголовков без тела), OPTIONS (получение поддерживаемых методов/параметров), PATCH (частичное обновление ресурса)
- URL — адрес эндпоинта, на который будет отправлен HTTP-запрос
- Authentication — способ аутентификации. n8n предоставляет два варианта: 1) Predefined Credential Type — набор преднастроенных шаблонов аутентификации; 2) Generic Credential Type — полностью настраиваемая аутентификация (так называемые «дженерики»).
- Credential Type или Generic Auth Type — поле выбора типа аутентификации, зависящее от выбранного ранее варианта в Authentication.
- Send Query Parameters — включение и настройка query-параметров (часть URL после ?, например ?limit=10)
- Send Headers — включение и настройка заголовков запроса (например, Content-Type, Authorization)
- Send Body — включение и настройка тела запроса (чаще всего используется в POST, PUT, PATCH)
Для опций Send Query Parameters, Send Headers, Send Body можно выбрать один из двух форматов отправки тела параметра:
- Using Fields Below — заполнение параметров в виде отдельных полей через интерфейс n8n
- Using JSON — задание параметров одним JSON-объектом
Выбор формата отправки параметров HTTP-запроса основывается на свойствах и спецификации API, на который отправляется запрос. Так, например, для отправки HTTP-запроса на извлечение текста статьи по ссылке в Python-микросервис article-parser используются следующие параметры HTTP Request-ноды:
- Method:POST
- URL: http://article-parser:8880/parse
- Authentication: None
- Send Query Parameters: Off
- Send Headers: Off
- Send Body: On
- Body Content Type: JSON
- Specify Body: Using JSON
- JSON:
"text":
"{{ $('Telegram Trigger').item.json.message.link_preview_options.url }}"
}
В приведённом примере HTTP Request-нода посылает POST-запрос на эндпоинт http://article-parser:8880/parse с телом запроса в формате JSON. Заголовки для аутентификации не передаются, так как этого не требует эндпоинт. Ниже — пример HTTP-запроса к API Obsidian, где уже передаются данные для аутентификации, как того требует спецификация API.
Пример отправки HTTP-запроса на добавление заметки в Obsidian с использованием HTTP Request-ноды:
- Method:PUT
- URL: https://caddy/obsidian/vault/{{ $('Reader').item.json.output.note_name }}.md
- Authentication: Generic Credential Type
- Generic Auth Type: Header auth
- Header Auth: Header Auth account
- Send Query Parameters: Off
- Send Headers: Off
- Send Body: On
- Body Content Type: JSON
- Specify Body: Using JSON
- JSON:
"text": "{{ jsonStringify($('Reader').item.json.output.text) }}"
}
В этом примере есть два важных момента, на которые нужно обратить внимание:
- Первый важный момент — URL, который указывает на Caddy, а не на Obsidian. Сделано это специально для того, чтобы обеспечить TLS-защищенность соединения. Caddy шифрует трафик и проксирует запрос в API-эндпоинт Obsidian.
- Второй важный момент — значение поля Generic Auth Type, которое задано как Header auth. При таком типе аутентификации будут передаваться заголовки с данными для аутентификации, которые указаны в Header Auth account — заранее созданных и сохраненных в n8n данных для подключения.
HTTP Request-нода — мощный и гибкий строительный элемент автоматизации n8n. Она позволяет вынести часть бизнес-логики во внешние сервисы или использовать n8n как мост для связи микросервисов между собой по API.
IF-нода
IF-нода позволяет строить ветвление логики workflow, основанное на различных условиях: сравнение элементов, наличие или отсутствие искомого элемента в массиве, точное или частичное соответствие, а также другие проверки.
IF-нода находится в основном разделе выбора нод:
В нашем проекте IF-нода используется в основном для проверки наличия текста и URL в сообщении.
Basic LLM Chain-нода
Работа с AI — это одна из основных фишек n8n, и для работы с AI в n8n есть разные специализированные ноды, предназначенные для решения общих и конкретных задач:
- AI Agent — основная нода для построения AI-агента с возможностью использовать инструменты (tools) и внешнее хранилище для контекста. Максимально полезна в многоэтапных задачах с применением внешних сервисов (например, PostgreSQL, AWS, GitHub)
- Basic LLM Chain — нода для обращения к модели без инструментов и внешней памяти. Подходит для простых одношаговых задач, где не нужно выполнять действия и хранить контекст
- Summarization Chain — нода для суммаризации текста: делает короткое summary по входному материалу (статье, заметке, документу). Применяется там, где нужно, чтобы AI читал большие документы или наборы документов
- Question and Answer Chain — нода для вопросов и ответов по входному тексту: отвечает на вопрос, опираясь на переданный контент. На базе этой ноды можно строить примитивных помощников для технической поддержки, если «скормить» AI корпоративную базу знаний
- LangChain Code — нода для реализации кастомной логики с помощью кода в рамках LangChain-пайплайнов
Все перечисленные выше ноды находятся в основном разделе выбора нод:
В нашем workflow используется Basic LLM Chain для решения задачи чтения и создания summary. Мы выбрали Basic LLM Chain, а не Summarization Chain, по причине того, что Basic LLM Chain поддерживает возможность задать нужный формат вывода результата работы AI-агента. Для нашей задачи — это важный параметр, так как AI генерирует ещё и короткое название файла заметки для Obsidian на базе прочитанного материала.
Выходной формат данных в виде JSON выглядит так:
"note_name": "string",
"text": "string"
}
Где note_name — это название файла заметки на английском языке без пробелов и спецсимволов, а text — текст заметки, оформленный по следующему шаблону:
- Ссылка на статью: {{ $('If_link_exists').item.json.message.text }}
- О чем статья (h2)
- Теоретическая часть (h2)
- Практическая часть (h2)
- Вывод (h2)
Так как формат вывода для нашего проекта критически важен, в связке с Basic LLM Chain мы применили ещё несколько специальных нод: Auto-fixing Output Parser и Structured Output Parser. Эти ноды нужны для строгого соблюдения выходного формата данных. Связка нод работает так: Basic LLM Chain формирует текст в соответствии с JSON-моделью, указанной в Structured Output Parser, а потом полученный JSON отправляется в ноду Auto-fixing Output Parser, которая подключена к AI так же, как и Basic LLM Chain. Там ответ модели ещё раз сравнивается с моделью в Structured Output Parser: если форма ответа совпадает с референсной моделью — результат работы AI идёт дальше по workflow, если же форма ответа не совпадает с заданной моделью — Auto-fixing Output Parser подгоняет ответ под заданную модель с помощью AI.
В workflow конструкция из Basic LLM chain, Auto-fixing Output Parser и Structured Output Parser выглядит следующим образом:
Выбор правильной ноды для работы с AI экономит ресурсы и снижает количество возможных ошибок.
Telegram-нода
n8n поддерживает большое количество специализированных Telegram-нод для разных задач. Например, есть Telegram-ноды, которые реагируют на определённые действия пользователя в Telegram, а другие Telegram-ноды используются для отправки сообщений пользователю в Telegram.
В проекте используются два вида Telegram-нод.
1. Telegram trigger on message-нода активирует workflow, когда пользователь отправляет сообщение в Telegram-бота. Находится нода в разделе Telegram → Triggers → On message.
У ноды Telegram trigger on message есть важное свойство — она умеет разбирать входящие сообщения от пользователя на составные части и в том числе определять URL. Нам не нужно находить ссылку на статью в тексте сообщения пользователя сторонними утилитами или JS-кодом — мы просто используем переменную {{ $json.message.link_preview_options.url }}: она хранит нужный нам URL.
2. Telegram: send a text message-нода доставки сообщения пользователю через Telegram-бот. Находится нода в разделе Telegram → Message actions → Send a text message.
Ноду Telegram: send a text message мы используем для отправки пользователю сообщений о статусе работы workflow и об ошибках, если они случаются. Сейчас workflow работает в режиме отладки, и любая ошибка приводит к завершению работы.
У Telegram-нод, например у Telegram: send a text message-ноды, есть поле Chat ID — это девятизначный уникальный номер, который позволяет ботам и сторонним приложениям отправлять сообщения конкретным пользователям, в группы или каналы через Telegram Bot API.
Поле Chat ID обязательно для заполнения. Значение поля можно получить из Telegram trigger on message-ноды. Например, с помощью такой конструкции: {{ $node["Telegram Trigger"].json.message.chat.id }}, где $node["Telegram Trigger"] — ссылка на ноду, а .json.message.chat.id — обращение к переменной, хранящей ID чата.
Запуск и тестирование workflow
После скачивания нашего шаблона workflow нужно заменить параметры подключения в Telegram trigger on message и Basic LLM Chain. В Telegram trigger on message замените значение в поле Credential to connect with на ваш API-ключ, а в Basic LLM Chain — модель на ту, которую вы используете в Ollama.
После того как необходимые настройки будут выставлены, workflow можно запускать в тестовом режиме, чтобы убедиться, что нет ошибок. Запустить workflow в отладочном режиме можно нажатием кнопки Test Workflow:
Если тестовый режим не выявил ошибок и workflow успешно отработал, его можно запускать в продакшн-режиме. Для этого достаточно установить переключатель Inactive в режим Active:
На этом работа с workflow завершена. Теперь можно переходить в Telegram и отправлять боту URL на статью.
С чего все начиналось и что получилось в итоге?
Перед нами стояла задача создать Telegram-бота, который будет получать сообщения, в которых есть URL на статьи, читать их, делать саммари и сохранять заметку по прочитанной статье в Obsidian и Mkdocs для большего удобства.
Бэкенд был реализован на микросервисах с помощью Docker. В контейнерах были запущены: n8n, Obsidian, MkDocs, Netdata, article-parser, Caddy, caddy-perms-fixer, Ollama, Open WebUI. Стек был запущен на VPS следующей конфигурации: 8 CPU-ядер, 16 GB DDR, 300 GB SSD. Использовалась локально запущенная LLM (deepseek-r1) через Ollama.
Также был настроен домен и были настроены DNS-записи для субдоменов. В качестве HTTP-сервера мы использовали Caddy, который нам еще из коробки обеспечил TLS-шифрование между сервисами.
Также был настроен домен и были настроены DNS-записи для субдоменов. В качестве HTTP-сервера мы использовали Caddy, который нам еще из коробки обеспечил TLS-шифрование между сервисами.
При разработке workflow мы вынесли часть бизнес-логики в отдельный микросервис article-parser, который очищает текст исходной статьи от лишнего для нас HTML-кода. Сам workflow можно либо скачать в виде JSON-файла из этого репозитория, либо импортировать workflow в пустой шаблон n8n.
В итоге получился удобный и практичный Telegram-бот, который экономит время, а также удалось сделать крепкую техническую базу для развития ваших собственных проектов на базе n8n. Хорошим дополнением к уже наработанному материалу и опыту будут ссылки на нашу базу знаний, где есть раздел про Docker.
И, конечно же, вы всегда можете найти ВМ нужной вам конфигурации в разделе VPS/VDS.