first commit
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# Переопределения дефолтных значений из docker-compose.yml.
|
||||
# Файл опциональный — без него стек запустится с дефолтами.
|
||||
# Скопировать в .env и заполнить нужные значения.
|
||||
|
||||
# =============================================================================
|
||||
# n8n
|
||||
# =============================================================================
|
||||
|
||||
# URL для вебхуков (адрес, по которому n8n доступен снаружи)
|
||||
WEBHOOK_URL=http://YOUR_SERVER_IP:5678
|
||||
|
||||
# Пароль PostgreSQL
|
||||
DB_POSTGRESDB_PASSWORD=
|
||||
|
||||
# Токен аутентификации между n8n и task runner
|
||||
N8N_RUNNERS_AUTH_TOKEN=
|
||||
|
||||
# Ключ шифрования credentials в БД (зафиксировать после первого старта!)
|
||||
N8N_ENCRYPTION_KEY=
|
||||
|
||||
# Часовой пояс
|
||||
GENERIC_TIMEZONE=Europe/Moscow
|
||||
|
||||
# =============================================================================
|
||||
# Grafana
|
||||
# =============================================================================
|
||||
|
||||
GF_SECURITY_ADMIN_USER=admin
|
||||
GF_SECURITY_ADMIN_PASSWORD=
|
||||
|
||||
# =============================================================================
|
||||
# PostgreSQL
|
||||
# =============================================================================
|
||||
|
||||
DB_POSTGRESDB_DATABASE=n8n
|
||||
DB_POSTGRESDB_USER=postgres
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# Секреты — не коммитить
|
||||
.env
|
||||
|
||||
# Логи
|
||||
n8n/logs/*.log
|
||||
|
||||
# Бинарные данные
|
||||
n8n/shared/binaryData/
|
||||
|
||||
# Бэкапы с реальными данными (оставить только .gitkeep и служебные workflow)
|
||||
n8n/backup/workflows/*.json
|
||||
n8n/backup/credentials/*.json
|
||||
!n8n/backup/workflows/.gitkeep
|
||||
!n8n/backup/credentials/.gitkeep
|
||||
!n8n/backup/credentials/n8n_local.json
|
||||
!n8n/backup/credentials/header_n8n_local.json
|
||||
!n8n/backup/workflows/Backup_Workflows.json
|
||||
!n8n/backup/workflows/Git_Commit_Workflows.json
|
||||
!n8n/backup/workflows/Git_Pull_Workflows.json
|
||||
!n8n/backup/workflows/Backup_Сredentials.json
|
||||
@@ -0,0 +1,167 @@
|
||||
# n8n Docker Stack
|
||||
|
||||
Self-hosted n8n с PostgreSQL, Redis, Task Runners, Prometheus и Grafana — готовый production-стек с минимальными усилиями на настройку
|
||||
|
||||
## Зачем
|
||||
|
||||
- **Изоляция** — отключена телеметрия, шаблоны и внешние запросы к n8n.io
|
||||
- **Производительность** — queue mode с workers, внешний task runner для JS и Python
|
||||
- **Надёжность** — PostgreSQL вместо SQLite, Redis как брокер очередей
|
||||
- **Наблюдаемость** — Prometheus + Grafana с готовым дашбордом
|
||||
- **Воспроизводимость** — автоимпорт workflows и credentials при каждом старте
|
||||
- **Контроль версий** — встроенные workflows для экспорта и коммита в git
|
||||
|
||||
---
|
||||
|
||||
## Сервисы
|
||||
|
||||
| Сервис | Порт | Описание |
|
||||
|---|---|---|
|
||||
| n8n | 5678 | Основной сервер |
|
||||
| n8n-worker-1 | — | Worker для queue mode |
|
||||
| n8n-runner-1 | — | Внешний раннер JS + Python |
|
||||
| postgres | 5433 | База данных |
|
||||
| redis (Valkey) | — | Очередь Bull |
|
||||
| prometheus | 9090 | Сбор метрик |
|
||||
| grafana | 3000 | Дашборды |
|
||||
| fix-permissions | — | Однократная правка прав (uid 1000) |
|
||||
| n8n-import | — | Автоимпорт workflows и credentials при старте |
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Заполнить .env
|
||||
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Стек запускается без `.env` — все переменные имеют дефолты в `docker-compose.yml`.
|
||||
|
||||
---
|
||||
|
||||
## Деплой через Portainer
|
||||
|
||||
Portainer позволяет управлять стеком через UI без прямого доступа к серверу.
|
||||
|
||||
**Важно:** деплой через «URL» (одиночная ссылка на compose-файл) не работает — Portainer не скачивает вспомогательные файлы (`prometheus.yml`, `grafana/` и т.д.). Использовать только **Repository**.
|
||||
|
||||
### Требования
|
||||
|
||||
Portainer должен быть запущен так, чтобы **путь к данным на хосте и внутри контейнера совпадал**. Иначе Docker Engine не найдёт файлы клонированного репозитория — Portainer передаёт ему свои внутренние пути, а Docker ищет их на хосте.
|
||||
|
||||
```bash
|
||||
docker stop portainer && docker rm portainer
|
||||
|
||||
PORTAINER_DATA="$HOME/portainer/data"
|
||||
mkdir -p "$PORTAINER_DATA"
|
||||
|
||||
docker run -d \
|
||||
-p 9443:9443 \
|
||||
--name portainer \
|
||||
--restart=always \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v "$PORTAINER_DATA":"$PORTAINER_DATA" \
|
||||
portainer/portainer-ce:latest \
|
||||
--data "$PORTAINER_DATA"
|
||||
```
|
||||
|
||||
Флаг `--data` указывает Portainer хранить данные по тому же пути, что смонтирован с хоста — пути совпадают, Docker Engine находит файлы.
|
||||
|
||||
### Шаги
|
||||
|
||||
1. **Stacks → Add stack → Repository**
|
||||
2. Repository URL: адрес репозитория
|
||||
3. Compose path: `docker-compose.yml`
|
||||
4. Добавить переменные окружения или указать путь к `.env`
|
||||
Важно правильно настроить WEBHOOK_URL
|
||||
```
|
||||
# URL для вебхуков (адрес, по которому n8n доступен снаружи)
|
||||
WEBHOOK_URL=http://YOUR_SERVER_IP:5678
|
||||
```
|
||||
⚠️ Важно: Если указан неверный адрес, то Form Data и вебхуки не будут работать
|
||||
|
||||
5. Deploy the stack
|
||||
|
||||
> (Опционально) Дать права
|
||||
```sh
|
||||
sudo chmod 777 /home/bbr/portainer/data/compose
|
||||
```
|
||||
|
||||
### Документация
|
||||
|
||||
- [Установка Docker Compose](https://docs.n8n.io/hosting/installation/server-setups/docker-compose/)
|
||||
- [Stacks in Portainer](https://docs.portainer.io/user/docker/stacks/add)
|
||||
- [Git repository deployment](https://docs.portainer.io/user/docker/stacks/add#option-3-git-repository)
|
||||
|
||||
---
|
||||
|
||||
## Встроенные workflows
|
||||
|
||||
В `n8n/backup/workflows/` лежат служебные workflow, которые импортируются автоматически при каждом старте.
|
||||
|
||||
| Файл | Назначение |
|
||||
|---|---|
|
||||
| `Backup_Workflows.json` | Экспорт всех workflow в `n8n/shared/` |
|
||||
| `Backup_Сredentials.json` | Экспорт всех credentials в `n8n/shared/` |
|
||||
| `Git_Commit_Workflows.json` | Коммит экспортированных файлов в git |
|
||||
| `Git_Pull_Workflows.json` | Pull изменений из git-репозитория |
|
||||
|
||||
### Настройка credentials
|
||||
|
||||
Встроенные workflows обращаются к API локального n8n. Для работы необходимо:
|
||||
|
||||
1. Сгенерировать API ключ: n8n → **Settings → API → Create an API key**
|
||||
2. Создать credentials типа **n8n API**:
|
||||
- **URL**: `http://n8n:5678/api/v1`
|
||||
- **API Key**: ключ из шага 1
|
||||
3. Сохранить credentials в файл `n8n/backup/credentials/n8n_local.json`
|
||||
|
||||
### Защита credentials от перезаписи
|
||||
|
||||
n8n CLI не имеет встроенного `--skipExisting` для импорта credentials. Скрипт реализует это через маркер-файлы в `n8n_storage` volume (`/home/node/.n8n/imported/`).
|
||||
|
||||
| Тип | Поведение при повторном деплое |
|
||||
|---|---|
|
||||
| Credentials | Импортируются **один раз**, затем пропускаются |
|
||||
| Workflows | Перезаписываются **всегда** |
|
||||
|
||||
Принудительный переимпорт — задать `FORCE_IMPORT=true` в env сервиса `n8n-import`.
|
||||
|
||||
Для добавления своих workflow при старте — положить JSON в `n8n/backup/workflows/` или `n8n/backup/credentials/`.
|
||||
|
||||
### Документация
|
||||
|
||||
- [n8n Public API / Authentication](https://docs.n8n.io/api/authentication/)
|
||||
- [n8n CLI commands](https://docs.n8n.io/hosting/cli-commands/)
|
||||
- [Автоимпорт workflows](https://docs.n8n.io/hosting/cli-commands/#import-workflows-and-credentials)
|
||||
|
||||
---
|
||||
|
||||
## Политика хранения данных
|
||||
|
||||
| Что | Лимит |
|
||||
|---|---|
|
||||
| Данные выполнений | 7 дней / 5 000 записей |
|
||||
| История версий workflow | 30 дней |
|
||||
| Логи n8n (файлы) | 10 файлов × 20 MB = ~200 MB |
|
||||
| Логи Docker (все сервисы) | 3–5 файлов × 5–10 MB на сервис |
|
||||
| Prometheus TSDB | 15 дней / 1 GB |
|
||||
| Redis | 256 MB max (LRU) |
|
||||
|
||||
Бинарные данные в `n8n/shared/` автоматически не очищаются — при активном использовании нод Read/Write Files настроить очистку по расписанию.
|
||||
|
||||
### Документация
|
||||
|
||||
- [Execution data — pruning и retention](https://docs.n8n.io/hosting/scaling/execution-data/)
|
||||
- [Executions environment variables](https://docs.n8n.io/hosting/configuration/environment-variables/executions/)
|
||||
- [Workflow history](https://docs.n8n.io/workflows/history/)
|
||||
- [Логирование](https://docs.n8n.io/hosting/logging-monitoring/logging/)
|
||||
- [Environment Variables (все)](https://docs.n8n.io/hosting/configuration/environment-variables/)
|
||||
- [Task Runners](https://docs.n8n.io/hosting/configuration/task-runners/)
|
||||
- [Queue mode](https://docs.n8n.io/hosting/scaling/queue-mode/)
|
||||
- [Мониторинг n8n](https://docs.n8n.io/hosting/logging-monitoring/monitoring/)
|
||||
- [Prometheus](https://docs.n8n.io/hosting/configuration/configuration-examples/prometheus/)
|
||||
- [Аудит безопасности](https://docs.n8n.io/hosting/securing/security-audit/)
|
||||
@@ -0,0 +1,439 @@
|
||||
# тест тест
|
||||
|
||||
volumes:
|
||||
n8n_storage:
|
||||
postgres_data:
|
||||
valkey-data:
|
||||
grafana-data:
|
||||
prometheus_data:
|
||||
|
||||
x-n8n: &service-n8n
|
||||
image: n8nio/n8n:stable
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
WEBHOOK_URL: ${WEBHOOK_URL:-}
|
||||
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY:-}
|
||||
# Изоляция self-hosted
|
||||
# https://docs.n8n.io/hosting/configuration/configuration-examples/isolation/
|
||||
# =============================================================================
|
||||
# N8N_DIAGNOSTICS_ENABLED - Отправлять анонимную телеметрию (false отключает Ask AI в Code node). По умолчанию: true.
|
||||
# N8N_VERSION_NOTIFICATIONS_ENABLED - Уведомления о новых версиях. По умолчанию: true.
|
||||
# N8N_TEMPLATES_ENABLED - Включить шаблоны workflow. По умолчанию: false.
|
||||
# N8N_HIRING_BANNER_ENABLED - Баннер о вакансиях в консоли. По умолчанию: true.
|
||||
N8N_DIAGNOSTICS_ENABLED: false
|
||||
N8N_VERSION_NOTIFICATIONS_ENABLED: false
|
||||
N8N_TEMPLATES_ENABLED: false
|
||||
N8N_HIRING_BANNER_ENABLED: false
|
||||
|
||||
# Настройки логирования
|
||||
# https://docs.n8n.io/hosting/configuration/environment-variables/logs/
|
||||
# =============================================================================
|
||||
# N8N_LOG_LEVEL - Уровень логирования: info|warn|error|debug. По умолчанию: info.
|
||||
# N8N_LOG_OUTPUT - Куда выводить: console|file (через запятую). По умолчанию: console.
|
||||
# N8N_LOG_FORMAT - Формат: text|json. По умолчанию: text.
|
||||
# N8N_LOG_FILE_LOCATION - Путь к файлу лога. По умолчанию: ~/.n8n/logs/n8n.log.
|
||||
# N8N_LOG_FILE_SIZE_MAX - Максимальный размер файла лога (MB). По умолчанию: 16.
|
||||
# DB_LOGGING_ENABLED - Логирование запросов к БД. По умолчанию: false.
|
||||
# DB_LOGGING_OPTIONS - Уровень логирования БД: query|error|schema|warn|info|log|all. По умолчанию: error.
|
||||
# CODE_ENABLE_STDOUT - Выводить console.log из Code node в stdout. По умолчанию: false.
|
||||
|
||||
N8N_LOG_LEVEL: info
|
||||
N8N_LOG_OUTPUT: console,file
|
||||
N8N_LOG_FORMAT: json
|
||||
N8N_LOG_FILE_LOCATION: /data/logs/n8n.log
|
||||
N8N_LOG_FILE_SIZE_MAX: 20
|
||||
N8N_LOG_FILE_COUNT_MAX: 10
|
||||
DB_LOGGING_ENABLED: true
|
||||
DB_LOGGING_OPTIONS: error
|
||||
CODE_ENABLE_STDOUT: false
|
||||
|
||||
# Персонализация
|
||||
# =============================================================================
|
||||
# N8N_PERSONALIZATION_ENABLED - Вопросы персонализации при первом запуске. По умолчанию: true.
|
||||
# N8N_HIDE_USAGE_PAGE - Скрыть страницу использования и планов. По умолчанию: false.
|
||||
N8N_PERSONALIZATION_ENABLED: false
|
||||
N8N_HIDE_USAGE_PAGE: true
|
||||
|
||||
# Настройки узлов (Nodes)
|
||||
# https://docs.n8n.io/hosting/configuration/environment-variables/nodes/
|
||||
# NODES_EXCLUDE: Список узлов, которые не нужно загружать (блокировка). Пример: ["n8n-nodes-baseexecuteCommand"].
|
||||
NODES_EXCLUDE: ${NODES_EXCLUDE:-[]}
|
||||
|
||||
# Настройки безопасности
|
||||
# https://docs.n8n.io/hosting/configuration/environment-variables/security/
|
||||
# N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES - Блокировать доступ к файлам .n8n и конфигам. По умолчанию: true.
|
||||
# N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS - Права 0600 на файл настроек. По умолчанию: false.
|
||||
# N8N_RESTRICT_FILE_ACCESS_TO - Директории с разрешенным доступом к файлам (через ;). По умолчанию: "".
|
||||
|
||||
N8N_BLOCK_FILE_ACCESS_TO_N8N_FILES: true
|
||||
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: false
|
||||
N8N_RESTRICT_FILE_ACCESS_TO: /data/shared;/data/logs
|
||||
|
||||
# Настройки часового пояса и локализации
|
||||
# https://docs.n8n.io/hosting/configuration/environment-variables/timezone-localization/
|
||||
# =============================================================================
|
||||
# GENERIC_TIMEZONE - Часовой пояс (важно для Schedule/Cron узлов). По умолчанию: America/New_York.
|
||||
# N8N_DEFAULT_LOCALE - Локаль интерфейса (региональные идентификаторы не поддерживаются). По умолчанию: en.
|
||||
GENERIC_TIMEZONE: ${GENERIC_TIMEZONE:-Europe/Moscow}
|
||||
N8N_DEFAULT_LOCALE: ${N8N_DEFAULT_LOCALE:-en}
|
||||
|
||||
# Настройки базы данных PostgreSQL
|
||||
# https://docs.n8n.io/hosting/configuration/environment-variables/database/
|
||||
# =============================================================================
|
||||
# -- Параметры подключения
|
||||
# DB_POSTGRESDB_DATABASE - Имя базы данных PostgreSQL(по умолчанию: n8n)
|
||||
# DB_POSTGRESDB_HOST - Хост PostgreSQL сервера(по умолчанию: localhost)
|
||||
# DB_POSTGRESDB_PORT - Порт PostgreSQL сервера(по умолчанию: 5432)
|
||||
# DB_POSTGRESDB_USER - Имя пользователя PostgreSQL(по умолчанию: postgres)
|
||||
# DB_POSTGRESDB_PASSWORD - Пароль PostgreSQL(обязательное поле, нет значения по умолчанию)
|
||||
|
||||
DB_TYPE: postgresdb
|
||||
DB_POSTGRESDB_DATABASE: ${DB_POSTGRESDB_DATABASE:-n8n}
|
||||
DB_POSTGRESDB_HOST: ${DB_POSTGRESDB_HOST:-postgres}
|
||||
DB_POSTGRESDB_PORT: ${DB_POSTGRESDB_PORT:-5432}
|
||||
DB_POSTGRESDB_USER: ${DB_POSTGRESDB_USER:-postgres}
|
||||
DB_POSTGRESDB_PASSWORD: ${DB_POSTGRESDB_PASSWORD:-your_secure_password_here}
|
||||
|
||||
# Настройки Task Runner
|
||||
# https://docs.n8n.io/hosting/configuration/environment-variables/task-runners/
|
||||
# =============================================================================
|
||||
# N8N_RUNNERS_ENABLED - Включить task runners (рекомендуется для production). По умолчанию: false.
|
||||
# N8N_RUNNERS_MODE - Режим: internal|external. По умолчанию: internal.
|
||||
# N8N_RUNNERS_BROKER_LISTEN_ADDRESS - Адрес брокера (127.0.0.1 для internal, 0.0.0.0 для external). По умолчанию: 127.0.0.1.
|
||||
# N8N_RUNNERS_AUTH_TOKEN - Токен аутентификации (обязателен для external, авто для internal). По умолчанию: "".
|
||||
# N8N_RUNNERS_MAX_CONCURRENCY - Одновременных задач на runner'е. По умолчанию: 5.
|
||||
# N8N_RUNNERS_TASK_TIMEOUT - Макс. время выполнения задачи (сек), после — runner перезапускается. По умолчанию: 300.
|
||||
# N8N_RUNNERS_INSECURE_MODE - Отключить все меры безопасности (НЕ ДЛЯ PRODUCTION!). По умолчанию: false.
|
||||
# N8N_RUNNERS_TASK_REQUEST_TIMEOUT - Таймаут ожидания свободного runner'а (сек). По умолчанию: 20.
|
||||
|
||||
N8N_RUNNERS_ENABLED: true
|
||||
N8N_RUNNERS_MODE: external
|
||||
N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN:-your-secret-here}
|
||||
N8N_RUNNERS_BROKER_PORT: 5679
|
||||
N8N_RUNNERS_BROKER_LISTEN_ADDRESS: 0.0.0.0
|
||||
N8N_RUNNERS_MAX_CONCURRENCY: ${N8N_RUNNERS_MAX_CONCURRENCY:-5}
|
||||
N8N_RUNNERS_TASK_TIMEOUT: ${N8N_RUNNERS_TASK_TIMEOUT:-300}
|
||||
N8N_RUNNERS_INSECURE_MODE: ${N8N_RUNNERS_INSECURE_MODE:-false}
|
||||
N8N_RUNNERS_TASK_REQUEST_TIMEOUT: ${N8N_RUNNERS_TASK_REQUEST_TIMEOUT:-20}
|
||||
|
||||
# Настройки режима очереди (Queue mode)
|
||||
# https://docs.n8n.io/hosting/configuration/environment-variables/queue-mode/
|
||||
# =============================================================================
|
||||
# EXECUTIONS_MODE - Режим выполнения: regular|queue. По умолчанию: regular.
|
||||
# OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS - Ручные запуски на worker'ах. По умолчанию: false.
|
||||
# QUEUE_BULL_REDIS_HOST - Хост Redis. По умолчанию: localhost.
|
||||
# QUEUE_BULL_REDIS_PORT - Порт Redis. По умолчанию: 6379.
|
||||
# QUEUE_HEALTH_CHECK_ACTIVE - Health checks. По умолчанию: false.
|
||||
|
||||
EXECUTIONS_MODE: queue
|
||||
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS: true
|
||||
QUEUE_BULL_REDIS_HOST: ${REDIS_HOST:-redis}
|
||||
QUEUE_BULL_REDIS_PORT: ${REDIS_PORT:-6379}
|
||||
QUEUE_HEALTH_CHECK_ACTIVE: true
|
||||
|
||||
# Настройки мониторинга
|
||||
# https://docs.n8n.io/hosting/configuration/environment-variables/endpoints/
|
||||
# =============================================================================
|
||||
|
||||
# N8N_METRICS - Включить endpoint /metrics. По умолчанию: false.
|
||||
# N8N_METRICS_PREFIX - Префикс для метрик. По умолчанию: n8n_.
|
||||
# N8N_METRICS_INCLUDE_DEFAULT_METRICS - Стандартные метрики системы и node.js. По умолчанию: true.
|
||||
# N8N_METRICS_INCLUDE_CACHE_METRICS - Метрики кэша. По умолчанию: false.
|
||||
# N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS - Метрики шины событий. По умолчанию: false.
|
||||
# N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL - Label с ID workflow. По умолчанию: false.
|
||||
# N8N_METRICS_INCLUDE_NODE_TYPE_LABEL - Label с типом узла. По умолчанию: false.
|
||||
# N8N_METRICS_INCLUDE_CREDENTIAL_TYPE_LABEL - Label с типом credentials. По умолчанию: false.
|
||||
# N8N_METRICS_INCLUDE_API_ENDPOINTS - Метрики для API endpoints. По умолчанию: false.
|
||||
# N8N_METRICS_INCLUDE_API_PATH_LABEL - Label с путем API. По умолчанию: false.
|
||||
# N8N_METRICS_INCLUDE_API_METHOD_LABEL - Label с HTTP методом. По умолчанию: false.
|
||||
# N8N_METRICS_INCLUDE_API_STATUS_CODE_LABEL - Label с HTTP статус-кодом. По умолчанию: false.
|
||||
# N8N_METRICS_INCLUDE_QUEUE_METRICS - Метрики очереди (для scaling mode). По умолчанию: false.
|
||||
# N8N_METRICS_QUEUE_METRICS_INTERVAL - Частота обновления метрик очереди (сек). По умолчанию: 20.
|
||||
|
||||
N8N_METRICS: ${N8N_METRICS:-true}
|
||||
N8N_METRICS_PREFIX: ${N8N_METRICS_PREFIX:-n8n_}
|
||||
N8N_METRICS_INCLUDE_DEFAULT_METRICS: ${N8N_METRICS_INCLUDE_DEFAULT_METRICS:-true}
|
||||
N8N_METRICS_INCLUDE_CACHE_METRICS: ${N8N_METRICS_INCLUDE_CACHE_METRICS:-false}
|
||||
N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS: ${N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS:-false}
|
||||
N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL: ${N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL:-true}
|
||||
N8N_METRICS_INCLUDE_NODE_TYPE_LABEL: ${N8N_METRICS_INCLUDE_NODE_TYPE_LABEL:-true}
|
||||
N8N_METRICS_INCLUDE_CREDENTIAL_TYPE_LABEL: ${N8N_METRICS_INCLUDE_CREDENTIAL_TYPE_LABEL:-false}
|
||||
N8N_METRICS_INCLUDE_API_ENDPOINTS: ${N8N_METRICS_INCLUDE_API_ENDPOINTS:-false}
|
||||
N8N_METRICS_INCLUDE_API_PATH_LABEL: ${N8N_METRICS_INCLUDE_API_PATH_LABEL:-false}
|
||||
N8N_METRICS_INCLUDE_API_METHOD_LABEL: ${N8N_METRICS_INCLUDE_API_METHOD_LABEL:-false}
|
||||
N8N_METRICS_INCLUDE_API_STATUS_CODE_LABEL: ${N8N_METRICS_INCLUDE_API_STATUS_CODE_LABEL:-false}
|
||||
N8N_METRICS_INCLUDE_QUEUE_METRICS: ${N8N_METRICS_INCLUDE_QUEUE_METRICS:-true}
|
||||
N8N_METRICS_QUEUE_METRICS_INTERVAL: ${N8N_METRICS_QUEUE_METRICS_INTERVAL:-20}
|
||||
|
||||
N8N_USER_MANAGEMENT_DISABLED: false
|
||||
N8N_SECURE_COOKIE: false
|
||||
|
||||
# Политика хранения данных выполнений
|
||||
# https://docs.n8n.io/hosting/configuration/environment-variables/executions/
|
||||
# =============================================================================
|
||||
# EXECUTIONS_DATA_MAX_AGE - Максимальный возраст данных (часы). По умолчанию: 336.
|
||||
# EXECUTIONS_DATA_PRUNE_MAX_COUNT - Максимум хранимых выполнений. По умолчанию: 10000.
|
||||
# N8N_WORKFLOW_HISTORY_PRUNE_TIME - Хранить версии workflow (часы, -1 = бессрочно). По умолчанию: -1.
|
||||
# N8N_INSIGHTS_COMPACTION_HOURLY_TO_DAILY_THRESHOLD_DAYS - Компактизация часовых → дневных (дни). По умолчанию: 90.
|
||||
# N8N_INSIGHTS_COMPACTION_DAILY_TO_WEEKLY_THRESHOLD_DAYS - Компактизация дневных → недельных (дни). По умолчанию: 180.
|
||||
|
||||
EXECUTIONS_DATA_PRUNE: true
|
||||
EXECUTIONS_DATA_MAX_AGE: 168 # 7 дней
|
||||
EXECUTIONS_DATA_PRUNE_MAX_COUNT: 5000
|
||||
EXECUTIONS_DATA_HARD_DELETE_BUFFER: 1
|
||||
EXECUTIONS_DATA_PRUNE_HARD_DELETE_INTERVAL: 15
|
||||
EXECUTIONS_DATA_PRUNE_SOFT_DELETE_INTERVAL: 60
|
||||
|
||||
N8N_WORKFLOW_HISTORY_PRUNE_TIME: 720 # 30 дней
|
||||
|
||||
N8N_INSIGHTS_COMPACTION_HOURLY_TO_DAILY_THRESHOLD_DAYS: 30
|
||||
N8N_INSIGHTS_COMPACTION_DAILY_TO_WEEKLY_THRESHOLD_DAYS: 60
|
||||
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "5"
|
||||
extra_hosts:
|
||||
- "kontur.bbr.ru:192.168.1.155"
|
||||
- "bankrupt.bbr.ru:192.168.1.155"
|
||||
- "forge.bbr.ru:192.168.1.155"
|
||||
- "forge2.bbr.ru:192.168.1.156"
|
||||
|
||||
x-n8n-runner: &service-n8n-runner
|
||||
build:
|
||||
context: ./n8n/runner
|
||||
dockerfile: Dockerfile.runner
|
||||
pull: true
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
services:
|
||||
fix-permissions:
|
||||
image: alpine:3
|
||||
container_name: fix-permissions
|
||||
restart: "no"
|
||||
user: root
|
||||
# n8n, n8n-worker-1, n8n-runner-1 запускаются от uid 1000 (node)
|
||||
# После клонирования через Portainer директории принадлежат root — исправляем
|
||||
command: chown -R 1000:1000 /data/shared /data/logs /backup
|
||||
volumes:
|
||||
- ./n8n/shared:/data/shared
|
||||
- ./n8n/logs:/data/logs
|
||||
- ./n8n/backup:/backup
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "1m"
|
||||
max-file: "1"
|
||||
|
||||
n8n:
|
||||
<<: *service-n8n
|
||||
container_name: n8n
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- n8n_storage:/home/node/.n8n
|
||||
- ./n8n/shared:/data/shared
|
||||
- ./n8n/logs:/data/logs
|
||||
ports:
|
||||
- "5678:5678"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:5678/healthz || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
depends_on:
|
||||
fix-permissions:
|
||||
condition: service_completed_successfully
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
n8n-worker-1:
|
||||
<<: *service-n8n
|
||||
command: worker
|
||||
container_name: n8n-worker-1
|
||||
volumes:
|
||||
- n8n_storage:/home/node/.n8n
|
||||
- ./n8n/shared:/data/shared
|
||||
- ./n8n/logs:/data/logs
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:5678/healthz || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
depends_on:
|
||||
fix-permissions:
|
||||
condition: service_completed_successfully
|
||||
n8n:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
n8n-runner-1:
|
||||
<<: *service-n8n-runner
|
||||
container_name: n8n-runner-1
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./n8n/shared:/data/shared
|
||||
- ./n8n/logs:/data/logs
|
||||
entrypoint:
|
||||
["/bin/sh", "-c", "/usr/local/bin/task-runner-launcher javascript python"]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://localhost:5680/healthz || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
environment:
|
||||
# N8N_RUNNERS_TASK_BROKER_URI - URI брокера задач. По умолчанию: http://127.0.0.1:5679.
|
||||
# GENERIC_TIMEZONE - Часовой пояс (важно для Schedule/Cron узлов). По умолчанию: America/New_York.
|
||||
# N8N_RUNNERS_AUTH_TOKEN - Токен аутентификации (обязателен для external, авто для internal). По умолчанию: "".
|
||||
# N8N_RUNNERS_LAUNCHER_LOG_LEVEL - Уровень логирования: debug|info|warn|error. По умолчанию: info.
|
||||
# N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT - Секунд бездействия до остановки runner'а. По умолчанию: 15.
|
||||
|
||||
N8N_RUNNERS_TASK_BROKER_URI: http://n8n-worker-1:5679
|
||||
GENERIC_TIMEZONE: ${GENERIC_TIMEZONE:-Europe/Moscow}
|
||||
N8N_RUNNERS_AUTH_TOKEN: ${N8N_RUNNERS_AUTH_TOKEN:-your-secret-here}
|
||||
N8N_RUNNERS_LAUNCHER_LOG_LEVEL: ${N8N_RUNNERS_LAUNCHER_LOG_LEVEL:-info}
|
||||
N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT: ${N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT:-15}
|
||||
depends_on:
|
||||
fix-permissions:
|
||||
condition: service_completed_successfully
|
||||
n8n-worker-1:
|
||||
condition: service_healthy
|
||||
|
||||
postgres:
|
||||
container_name: postgres
|
||||
image: postgres:${POSTGRES_VERSION:-17}
|
||||
restart: unless-stopped
|
||||
# autovacuum_vacuum_scale_factor=0.05 — запускать VACUUM когда мёртвых строк > 5% таблицы (по умолчанию 20%)
|
||||
command: >
|
||||
postgres
|
||||
-c autovacuum=on
|
||||
-c autovacuum_vacuum_scale_factor=0.05
|
||||
-c autovacuum_analyze_scale_factor=0.02
|
||||
ports:
|
||||
- "5433:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_POSTGRESDB_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${DB_POSTGRESDB_PASSWORD:-your_secure_password_here}
|
||||
POSTGRES_DB: ${DB_POSTGRESDB_DATABASE:-n8n}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
redis:
|
||||
container_name: redis
|
||||
image: docker.io/valkey/valkey:8-alpine
|
||||
# --save 30 1 — снапшот каждые 30 мин при ≥1 изменении
|
||||
# --maxmemory 256mb — жёсткий лимит памяти
|
||||
# --maxmemory-policy allkeys-lru — вытеснять редко используемые ключи при достижении лимита
|
||||
command: >
|
||||
valkey-server
|
||||
--save 30 1
|
||||
--loglevel warning
|
||||
--maxmemory 256mb
|
||||
--maxmemory-policy allkeys-lru
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 30s
|
||||
volumes:
|
||||
- valkey-data:/data
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- SETGID
|
||||
- SETUID
|
||||
- DAC_OVERRIDE
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 3s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "5m"
|
||||
max-file: "3"
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: prometheus
|
||||
restart: unless-stopped
|
||||
# --storage.tsdb.retention.time — удалять данные старше 15 дней
|
||||
# --storage.tsdb.retention.size — удалять при превышении 1 ГБ (что наступит первым)
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--storage.tsdb.retention.time=15d'
|
||||
- '--storage.tsdb.retention.size=1GB'
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- prometheus_data:/prometheus
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
depends_on:
|
||||
- n8n
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:11.2.0
|
||||
container_name: grafana
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
GF_SECURITY_ADMIN_USER: ${GF_SECURITY_ADMIN_USER:-admin}
|
||||
GF_SECURITY_ADMIN_PASSWORD: ${GF_SECURITY_ADMIN_PASSWORD:-admin}
|
||||
GF_LOG_LEVEL: warn
|
||||
GF_LOG_MODE: console
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning
|
||||
- ./grafana/dashboards:/var/lib/grafana/dashboards
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
depends_on:
|
||||
- prometheus
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
n8n-import:
|
||||
<<: *service-n8n
|
||||
container_name: n8n-import
|
||||
restart: "no"
|
||||
entrypoint: /bin/sh
|
||||
command: /scripts/import_workflows.sh
|
||||
volumes:
|
||||
- n8n_storage:/home/node/.n8n
|
||||
- ./n8n/shared:/data/shared
|
||||
- ./n8n/logs:/data/logs
|
||||
- ./n8n/backup:/backup
|
||||
- ./scripts/import_workflows.sh:/scripts/import_workflows.sh:ro
|
||||
depends_on:
|
||||
n8n:
|
||||
condition: service_healthy
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: "default"
|
||||
orgId: 1
|
||||
folder: ""
|
||||
type: file
|
||||
disableDeletion: false
|
||||
editable: true
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
jsonData:
|
||||
timeInterval: 15s
|
||||
editable: true
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"updatedAt": "2026-04-11T02:55:18.014Z",
|
||||
"createdAt": "2026-04-11T02:53:24.209Z",
|
||||
"id": "VcZvOMLi8tCfxhei",
|
||||
"name": "Header n8n local",
|
||||
"data": {
|
||||
"name": "X-N8N-API-KEY",
|
||||
"value": ""
|
||||
},
|
||||
"type": "httpHeaderAuth",
|
||||
"isManaged": false,
|
||||
"isGlobal": false,
|
||||
"isResolvable": false,
|
||||
"resolvableAllowFallback": false,
|
||||
"resolverId": null
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"updatedAt": "2026-04-10T14:06:12.051Z",
|
||||
"createdAt": "2026-04-10T13:58:36.675Z",
|
||||
"id": "aLPzwPxLHLLpPJIw",
|
||||
"name": "n8n local",
|
||||
"data": {
|
||||
"apiKey": "",
|
||||
"baseUrl": "http://n8n:5678/api/v1"
|
||||
},
|
||||
"type": "n8nApi",
|
||||
"isManaged": false,
|
||||
"isGlobal": false,
|
||||
"isResolvable": false,
|
||||
"resolvableAllowFallback": false,
|
||||
"resolverId": null
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
[
|
||||
{
|
||||
"updatedAt": "2026-03-06T10:45:21.272Z",
|
||||
"createdAt": "2026-03-06T10:12:36.819Z",
|
||||
"id": "bb4lGbKasex6fngs",
|
||||
"name": "Backup Workflows",
|
||||
"description": null,
|
||||
"active": false,
|
||||
"isArchived": false,
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Экспортирует все workflows при помощи [CLI команд](https://docs.n8n.io/hosting/cli-commands/#workflows)\n\nПолучает список через локальный API, затем для каждого выполняет команду экспорта.\nФайлы сохраняются в /data/shared/workflows с именами процессов",
|
||||
"height": 128,
|
||||
"width": 800
|
||||
},
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"position": [
|
||||
-256,
|
||||
-240
|
||||
],
|
||||
"typeVersion": 1,
|
||||
"id": "6e138476-8460-4d9d-9475-ba4fb5330c90",
|
||||
"name": "Sticky Note"
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
-256,
|
||||
-16
|
||||
],
|
||||
"id": "cdcd29a0-ea11-4a54-a2cc-3d673e463b14",
|
||||
"name": "Запуск процесса"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"filters": {},
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.n8n",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
-48,
|
||||
-16
|
||||
],
|
||||
"id": "5912c07d-3b77-43a9-bf8a-a438f4a751c5",
|
||||
"name": "Получение всех workflows",
|
||||
"credentials": {
|
||||
"n8nApi": {
|
||||
"id": "aLPzwPxLHLLpPJIw",
|
||||
"name": "n8n local"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=mkdir -p /data/shared/workflows &&\nn8n export:workflow --pretty --id=\"{{ $json.id }}\" --output=\"/data/shared/workflows/{{ $json.name }}.json\""
|
||||
},
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
400,
|
||||
0
|
||||
],
|
||||
"id": "edee2c90-217f-41b1-95f8-7b8f575c1d14",
|
||||
"name": "Экспорт workflow в shared"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.splitInBatches",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
160,
|
||||
-16
|
||||
],
|
||||
"id": "185e9856-ef99-46bd-8935-af5d75ef110a",
|
||||
"name": "Для каждого workflow"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Консольная команда [n8n export:workflow](https://docs.n8n.io/hosting/cli-commands/#workflows)\n| Flag | Описание |\n|------------|---------|\n| `--help` | Вывод справки |\n| `--all` | Экспортирует все workflows/credentials |\n| `--backup` | Устанавливает флаги `--all --pretty --separate` для создания резервной копии. Можно дополнительно указать `--output` |\n| `--id` | ID workflow для экспорта |\n| `--output` | Имя файла или директория (при использовании отдельных файлов) |\n| `--pretty` | Форматирует вывод для удобочитаемости |\n| `--separate` | Экспортирует по одному файлу на workflow (полезно для версионирования). Требует указания директории через `--output` |",
|
||||
"height": 288,
|
||||
"width": 816,
|
||||
"color": 7
|
||||
},
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"position": [
|
||||
-272,
|
||||
256
|
||||
],
|
||||
"typeVersion": 1,
|
||||
"id": "616d56ce-3144-4087-8d98-d9694dde5068",
|
||||
"name": "Sticky Note1"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Запуск процесса": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Получение всех workflows",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Получение всех workflows": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Для каждого workflow",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Экспорт workflow в shared": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Для каждого workflow",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Для каждого workflow": {
|
||||
"main": [
|
||||
[],
|
||||
[
|
||||
{
|
||||
"node": "Экспорт workflow в shared",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"binaryMode": "separate",
|
||||
"availableInMCP": false
|
||||
},
|
||||
"staticData": null,
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true
|
||||
},
|
||||
"pinData": {},
|
||||
"versionId": "8cda3060-529d-4255-a977-7fd7fb12fb85",
|
||||
"activeVersionId": null,
|
||||
"versionCounter": 88,
|
||||
"triggerCount": 0,
|
||||
"tags": [],
|
||||
"shared": [
|
||||
{
|
||||
"updatedAt": "2026-03-06T10:12:36.819Z",
|
||||
"createdAt": "2026-03-06T10:12:36.819Z",
|
||||
"role": "workflow:owner",
|
||||
"workflowId": "bb4lGbKasex6fngs",
|
||||
"projectId": "cyhwgvy6YLlE2sea",
|
||||
"project": {
|
||||
"updatedAt": "2026-02-25T14:36:20.598Z",
|
||||
"createdAt": "2026-02-25T14:34:43.191Z",
|
||||
"id": "cyhwgvy6YLlE2sea",
|
||||
"name": "Bolshakovsky Bolshakovsky <vazzzay@mail.ru>",
|
||||
"type": "personal",
|
||||
"icon": null,
|
||||
"description": null,
|
||||
"creatorId": "1e44e1f4-91c8-4fd2-96bc-df89e11c6414"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,120 @@
|
||||
[
|
||||
{
|
||||
"updatedAt": "2026-03-06T10:45:17.431Z",
|
||||
"createdAt": "2026-03-06T10:31:25.179Z",
|
||||
"id": "8MvuleYK3IywiA5o",
|
||||
"name": "Backup Сredentials",
|
||||
"description": null,
|
||||
"active": false,
|
||||
"isArchived": false,
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Экспортирует все credentials при помощи [CLI команд](https://docs.n8n.io/hosting/cli-commands/#credentials)\n\nФайлы сохраняются в /data/shared/credentials с id кредов",
|
||||
"height": 128,
|
||||
"width": 752
|
||||
},
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"position": [
|
||||
-432,
|
||||
-240
|
||||
],
|
||||
"typeVersion": 1,
|
||||
"id": "a052c437-b5a5-4f65-ae95-63cb72753cf4",
|
||||
"name": "Sticky Note"
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
-256,
|
||||
-16
|
||||
],
|
||||
"id": "fb44d479-bc43-4b16-b1ae-8a7e9110d4d9",
|
||||
"name": "Запуск процесса"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=mkdir -p /data/shared/credentials &&\nn8n export:credentials --all --backup --decrypted --output=/data/shared/credentials"
|
||||
},
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
-48,
|
||||
-16
|
||||
],
|
||||
"id": "9bc3a040-32a8-4968-9a71-91487c07ed30",
|
||||
"name": "Экспорт workflow в shared"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Консольная команда [n8n export:credentials](https://docs.n8n.io/hosting/cli-commands/#credentials)\n| Flag | Описание |\n|------|----------|\n| `--help` | Вывод справки |\n| `--all` | Экспортирует все workflows/credentials |\n| `--backup` | Устанавливает флаги `--all --pretty --separate` для создания резервной копии. Можно дополнительно указать `--output` |\n| `--id` | ID workflow для экспорта |\n| `--output` | Имя файла или директория (при использовании отдельных файлов) |\n| `--pretty` | Форматирует вывод для удобочитаемости |\n| `--separate` | Экспортирует по одному файлу на workflow (полезно для версионирования). Требует указания директории через `--output` |\n| `--decrypted` | Экспортирует credentials в открытом текстовом формате |",
|
||||
"height": 272,
|
||||
"width": 752,
|
||||
"color": 7
|
||||
},
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"position": [
|
||||
-416,
|
||||
192
|
||||
],
|
||||
"typeVersion": 1,
|
||||
"id": "d238f978-acb3-46b9-afce-f056803ddf01",
|
||||
"name": "Sticky Note1"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Запуск процесса": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Экспорт workflow в shared",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Экспорт workflow в shared": {
|
||||
"main": [
|
||||
[]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"binaryMode": "separate",
|
||||
"availableInMCP": false
|
||||
},
|
||||
"staticData": null,
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true
|
||||
},
|
||||
"pinData": {},
|
||||
"versionId": "915f573a-3ab0-4a4c-ba16-20b6f705bbc8",
|
||||
"activeVersionId": null,
|
||||
"versionCounter": 34,
|
||||
"triggerCount": 0,
|
||||
"tags": [],
|
||||
"shared": [
|
||||
{
|
||||
"updatedAt": "2026-03-06T10:31:25.179Z",
|
||||
"createdAt": "2026-03-06T10:31:25.179Z",
|
||||
"role": "workflow:owner",
|
||||
"workflowId": "8MvuleYK3IywiA5o",
|
||||
"projectId": "cyhwgvy6YLlE2sea",
|
||||
"project": {
|
||||
"updatedAt": "2026-02-25T14:36:20.598Z",
|
||||
"createdAt": "2026-02-25T14:34:43.191Z",
|
||||
"id": "cyhwgvy6YLlE2sea",
|
||||
"name": "Bolshakovsky Bolshakovsky <vazzzay@mail.ru>",
|
||||
"type": "personal",
|
||||
"icon": null,
|
||||
"description": null,
|
||||
"creatorId": "1e44e1f4-91c8-4fd2-96bc-df89e11c6414"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,854 @@
|
||||
{
|
||||
"name": "Git Pull Workflows",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Точка входа\n\nДва способа запустить workflow:\n\n**Форма** — основной способ. Пользователь заполняет данные репозитория, ветку, папку и переключатель **Активировать workflow** — разрешить ли активацию вообще.\n\n**Ручной запуск** — для отладки без открытия формы. Тестовые данные задаются в ноде «Тестовые данные»",
|
||||
"height": 964,
|
||||
"width": 588,
|
||||
"color": 7
|
||||
},
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
5136,
|
||||
496
|
||||
],
|
||||
"id": "987b6189-d7c7-4b68-86cf-b568c1f92096",
|
||||
"name": "Зона: Точка входа"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Подготовка и git операции\n\nПарсим данные формы и строим переменные:\n- **remoteUrl** — URL с кредами вида `http://user:pat@host/org/repo.git`. Используется только для git операций\n- **cleanRemoteUrl** — URL без кредов\n- **localFolder** — локальная папка репозитория\n\n**Git Clone / Pull:**\n- Папки нет или нет `.git` -> `git clone --branch`\n- Папка есть → обновляем remote URL, `git fetch`, `git checkout`, `git pull`\n\n⚠️ После операций сразу очищаем remote от кредов\n\n**Pull успешен?** — проверяем `exitCode === 0`. При ошибке показываем форму с текстом из stderr.",
|
||||
"height": 964,
|
||||
"width": 600,
|
||||
"color": 5
|
||||
},
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
5776,
|
||||
496
|
||||
],
|
||||
"id": "523cc286-1ae6-4647-bbe3-f36e3164a3b1",
|
||||
"name": "Зона: Подготовка и git pull"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Чтение файлов и определение статуса\n\nПолучаем список всех workflow из n8n через API, затем читаем все `.json` файлы из локальной папки репозитория.\n\nДля каждого файла определяем статус:\n- **NEW** — workflow с таким именем не найден в n8n\n- **CHANGED** — найден, но `updatedAt` отличается\n- **UNCHANGED** — найден и `updatedAt` совпадает — пропускаем\n\nДополнительно читаем поле **`active`** из JSON файла — оно определяет нужно ли активировать workflow после импорта (если разрешено глобально).\n\nФайлы которые не удалось распарсить пропускаются с выводом в console.log.",
|
||||
"height": 964,
|
||||
"width": 600,
|
||||
"color": 6
|
||||
},
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
6432,
|
||||
496
|
||||
],
|
||||
"id": "6f3247a2-d433-4f75-ba92-832b33e5a584",
|
||||
"name": "Зона: Чтение файлов"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Импорт через CLI\n\nДля каждого файла:\n\n**Switch — маршрутизация по статусу:**\n- `NEW` → импортируем через `n8n import:workflow`\n- `CHANGED` → импортируем через `n8n import:workflow`\n- `UNCHANGED` → пропускаем, сразу пишем результат\n\n**Разбор результата импорта** — парсим stdout, извлекаем статус успеха.\n\n**Нужно активировать?** — IF нода, все три условия должны выполняться:\n1. `activate = true` — пользователь разрешил активацию в форме\n2. `wfActive = true` — в JSON файле workflow был `\"active\": true`\n3. `success = true` — импорт прошёл без ошибок\n\n- Да → **Активировать workflow** (`n8n update:workflow --id=<id> --active=true`)\n- Нет → сразу **Записать результат**",
|
||||
"height": 964,
|
||||
"width": 1040,
|
||||
"color": 4
|
||||
},
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
7088,
|
||||
496
|
||||
],
|
||||
"id": "e108e18e-20b7-4a4c-b6dd-c21c7ff13b52",
|
||||
"name": "Зона: Поиск и импорт"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Итоговый отчёт\n\nПосле завершения цикла собираем статистику по всем файлам:\n- ✅ **Создано** — NEW + imported\n- 🔄 **Обновлено** — CHANGED + imported\n- ➖ **Пропущено** — UNCHANGED\n- ❌ **Ошибок** — не импортировались\n- ▶️ **Активировано** — workflow у которых `active: true` в файле и пользователь разрешил активацию\n\nВ детальном списке каждый активированный workflow помечается ▶️.\n\n⚠️ После импорта обязательно проверьте:\n1. Credentials в нодах — нужно перепривязать вручную\n2. Webhook URL — могли измениться",
|
||||
"height": 964,
|
||||
"width": 592,
|
||||
"color": 3
|
||||
},
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
8192,
|
||||
496
|
||||
],
|
||||
"id": "8fc98c91-a6ab-42a3-af2f-b4f78b7b31af",
|
||||
"name": "Зона: Результат"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"formTitle": "Импорт Workflow из Git",
|
||||
"formDescription": "Заполните данные для загрузки workflow из репозитория в n8n",
|
||||
"formFields": {
|
||||
"values": [
|
||||
{
|
||||
"fieldLabel": "Username",
|
||||
"placeholder": "gitea_username",
|
||||
"requiredField": true
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Personal Access Token",
|
||||
"fieldType": "password",
|
||||
"placeholder": "your_personal_access_token",
|
||||
"requiredField": true
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Repository URL",
|
||||
"placeholder": "https://<URL Repo>",
|
||||
"defaultValue": "https://vcs.bbr.ru",
|
||||
"requiredField": true
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Organization",
|
||||
"placeholder": "Название организации",
|
||||
"defaultValue": "event_forge",
|
||||
"requiredField": true
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Repository Name",
|
||||
"placeholder": "my-project",
|
||||
"defaultValue": "workflows",
|
||||
"requiredField": true
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Branch",
|
||||
"fieldType": "dropdown",
|
||||
"defaultValue": "main",
|
||||
"fieldOptions": {
|
||||
"values": [
|
||||
{
|
||||
"option": "main"
|
||||
},
|
||||
{
|
||||
"option": "develop"
|
||||
},
|
||||
{
|
||||
"option": "custom"
|
||||
}
|
||||
]
|
||||
},
|
||||
"requiredField": true
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Custom Branch Name",
|
||||
"placeholder": "Полное имя ветки если выбран custom"
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Export Folder",
|
||||
"placeholder": "/data/shared",
|
||||
"defaultValue": "/data/shared",
|
||||
"requiredField": true
|
||||
},
|
||||
{
|
||||
"fieldLabel": "Активировать workflow",
|
||||
"fieldType": "radio",
|
||||
"defaultValue": "0",
|
||||
"fieldOptions": {
|
||||
"values": [
|
||||
{
|
||||
"option": "Да"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"responseMode": "lastNode",
|
||||
"options": {
|
||||
"path": "99ac2ec1-b958-4522-99f3-136a8f080ed1",
|
||||
"customCss": ":root {\n --font-size-body: 13px;\n --font-size-label: 13px;\n --font-size-test-notice: 12px;\n --font-size-input: 13px;\n --font-size-header: 20px;\n --font-size-paragraph: 13px;\n --font-size-link: 12px;\n --font-size-error: 12px;\n --font-size-html-h1: 28px;\n --font-size-html-h2: 20px;\n --font-size-html-h3: 16px;\n --font-size-html-h4: 13px;\n --font-size-html-h5: 12px;\n --font-size-html-h6: 11px;\n --font-size-subheader: 13px;\n --color-background: #1b1f23;\n --color-test-notice-text: #d29922;\n --color-test-notice-bg: #272115;\n --color-test-notice-border: #5a4010;\n --color-card-bg: #22272e;\n --color-card-border: #373e47;\n --color-card-shadow: rgba(0,0,0,0.4);\n --color-link: #539bf5;\n --color-html-link: #539bf5;\n --color-header: #cdd9e5;\n --color-header-subtext: #768390;\n --color-label: #adbac7;\n --color-input-border: #444c56;\n --color-input-text: #cdd9e5;\n --color-input-bg: #1c2128;\n --color-focus-border: #539bf5;\n --color-submit-btn-bg: #347d39;\n --color-submit-btn-text: #cdd9e5;\n --color-error: #e5534b;\n --color-required: #e5534b;\n --color-clear-button-bg: #636e7b;\n --color-html-text: #adbac7;\n --border-radius-card: 6px;\n --border-radius-input: 6px;\n --border-radius-clear-btn: 50%;\n --card-border-radius: 6px;\n --padding-container-top: 24px;\n --padding-card: 24px;\n --padding-test-notice-vertical: 10px;\n --padding-test-notice-horizontal: 16px;\n --margin-bottom-card: 16px;\n --padding-form-input: 12px;\n --card-padding: 24px;\n --card-margin-bottom: 16px;\n --container-width: 448px;\n --submit-btn-height: 44px;\n --checkbox-size: 16px;\n --box-shadow-card: 0px 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.03);\n --opacity-placeholder: 0.35;\n}"
|
||||
}
|
||||
},
|
||||
"id": "c6aa7731-f958-4895-93e0-99960dab73e9",
|
||||
"name": "Форма запроса",
|
||||
"type": "n8n-nodes-base.formTrigger",
|
||||
"typeVersion": 2.2,
|
||||
"position": [
|
||||
5280,
|
||||
912
|
||||
],
|
||||
"webhookId": "99ac2ec1-b958-4522-99f3-136a8f080ed1"
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
5280,
|
||||
1136
|
||||
],
|
||||
"id": "0bb92efd-de10-4f1a-9372-9f41e9b74b8e",
|
||||
"name": "Ручной запуск (отладка)"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "raw",
|
||||
"jsonOutput": "{\n \"Username\": \"gitea\",\n \"Personal Access Token\": \"0f276ee359b823464680037cb9b62f3558796f95\",\n \"Repository URL\": \"http://192.168.1.155:3001\",\n \"Repository Name\": \"workflows\",\n \"Organization\": \"event_forge\",\n \"Branch\": \"main\",\n \"Custom Branch Name\": \"\",\n \"Export Folder\": \"/data/shared\",\n \"Активировать workflow\": false\n}\n",
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
5456,
|
||||
1136
|
||||
],
|
||||
"id": "1685976a-f037-4f4e-ad5b-6fed8c808fb8",
|
||||
"name": "Тестовые данные"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const body = $input.first().json;\n\nconst username = (body['Username'] || '').trim();\nconst pat = (body['Personal Access Token'] || '').trim();\nconst repoUrl = (body['Repository URL'] || '').trim().replace(/\\/$/, '');\nconst repoName = (body['Repository Name'] || '').trim();\nconst org = (body['Organization'] || '').trim();\nconst exportFolder = (body['Export Folder'] || '/data/shared').trim().replace(/\\/$/, '');\n\n// Определяем ветку\nconst branchType = body['Branch'];\nconst customBranch = (body['Custom Branch Name'] || '').trim();\nlet branch;\nif (branchType === 'custom') {\n if (!customBranch) throw new Error('Custom Branch Name обязателен при выборе custom');\n branch = customBranch;\n} else {\n branch = branchType;\n}\n\nconst authUrl = repoUrl.replace(/^(https?:\\/\\/)/, `$1${encodeURIComponent(username)}:${encodeURIComponent(pat)}@`);\nconst remoteUrl = `${authUrl}/${org}/${repoName}.git`;\nconst cleanRemoteUrl = `${repoUrl}/${org}/${repoName}.git`;\nconst localFolder = `${exportFolder}/${org}/${repoName}`;\nconst activate = !!(body['Активировать workflow']);\n\nreturn [{\n json: {\n username,\n pat,\n repoUrl,\n repoName,\n org,\n branch,\n remoteUrl,\n cleanRemoteUrl,\n localFolder,\n activate,\n exportFolder,\n }\n}];"
|
||||
},
|
||||
"id": "74a303da-5491-4623-a884-298621a18122",
|
||||
"name": "Подготовка переменных",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
5856,
|
||||
912
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=LOCAL_FOLDER=\"{{ $json.localFolder }}\"\nREMOTE_URL=\"{{ $json.remoteUrl }}\"\nCLEAN_URL=\"{{ $json.cleanRemoteUrl }}\"\nBRANCH=\"{{ $json.branch }}\"\n\nif [ ! -d \"$LOCAL_FOLDER/.git\" ]; then\n # Папки нет или нет .git — клонируем заново\n rm -rf \"$LOCAL_FOLDER\"\n mkdir -p \"$(dirname $LOCAL_FOLDER)\"\n git clone --branch \"$BRANCH\" \"$REMOTE_URL\" \"$LOCAL_FOLDER\" 2>&1\n cd \"$LOCAL_FOLDER\"\nelse\n # Папка есть — обновляем remote и подтягиваем\n cd \"$LOCAL_FOLDER\"\n CURRENT_REMOTE=$(git remote get-url origin 2>/dev/null || echo \"\")\n if [ \"$CURRENT_REMOTE\" != \"$REMOTE_URL\" ]; then\n git remote set-url origin \"$REMOTE_URL\"\n fi\n git fetch origin 2>&1\n git checkout \"$BRANCH\" 2>&1 || git checkout -b \"$BRANCH\" \"origin/$BRANCH\" 2>&1\n git pull origin \"$BRANCH\" 2>&1\nfi\n\n# Очищаем remote от кредов сразу после операций\ngit remote set-url origin \"$CLEAN_URL\"\n\necho \"PULL_SUCCESS: $BRANCH | $LOCAL_FOLDER\""
|
||||
},
|
||||
"id": "5f20be07-92b1-4ba5-91b6-a95b0947e233",
|
||||
"name": "Git Clone / Pull",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
6032,
|
||||
912
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict",
|
||||
"version": 3
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "pull-success-check",
|
||||
"leftValue": "={{ $json.exitCode }}",
|
||||
"rightValue": 0,
|
||||
"operator": {
|
||||
"type": "number",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.3,
|
||||
"position": [
|
||||
6208,
|
||||
912
|
||||
],
|
||||
"id": "010d52da-670b-40a0-9177-fb87cb9fa4bf",
|
||||
"name": "Pull успешен?"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "completion",
|
||||
"completionTitle": "Ошибка git",
|
||||
"completionMessage": "=Не удалось получить данные из репозитория:\n\n{{ $json.stderr }}",
|
||||
"limitWaitTime": true,
|
||||
"resumeUnit": "minutes",
|
||||
"options": {
|
||||
"customCss": ":root {\n --font-size-body: 13px;\n --font-size-label: 13px;\n --font-size-test-notice: 12px;\n --font-size-input: 13px;\n --font-size-header: 20px;\n --font-size-paragraph: 13px;\n --font-size-link: 12px;\n --font-size-error: 12px;\n --font-size-html-h1: 28px;\n --font-size-html-h2: 20px;\n --font-size-html-h3: 16px;\n --font-size-html-h4: 13px;\n --font-size-html-h5: 12px;\n --font-size-html-h6: 11px;\n --font-size-subheader: 13px;\n --color-background: #1b1f23;\n --color-test-notice-text: #d29922;\n --color-test-notice-bg: #272115;\n --color-test-notice-border: #5a4010;\n --color-card-bg: #22272e;\n --color-card-border: #373e47;\n --color-card-shadow: rgba(0,0,0,0.4);\n --color-link: #539bf5;\n --color-html-link: #539bf5;\n --color-header: #cdd9e5;\n --color-header-subtext: #768390;\n --color-label: #adbac7;\n --color-input-border: #444c56;\n --color-input-text: #cdd9e5;\n --color-input-bg: #1c2128;\n --color-focus-border: #539bf5;\n --color-submit-btn-bg: #347d39;\n --color-submit-btn-text: #cdd9e5;\n --color-error: #e5534b;\n --color-required: #e5534b;\n --color-clear-button-bg: #636e7b;\n --color-html-text: #adbac7;\n --border-radius-card: 6px;\n --border-radius-input: 6px;\n --border-radius-clear-btn: 50%;\n --card-border-radius: 6px;\n --padding-container-top: 24px;\n --padding-card: 24px;\n --padding-test-notice-vertical: 10px;\n --padding-test-notice-horizontal: 16px;\n --margin-bottom-card: 16px;\n --padding-form-input: 12px;\n --card-padding: 24px;\n --card-margin-bottom: 16px;\n --container-width: 448px;\n --submit-btn-height: 44px;\n --checkbox-size: 16px;\n --box-shadow-card: 0px 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.03);\n --opacity-placeholder: 0.35;\n}"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.form",
|
||||
"typeVersion": 2.5,
|
||||
"position": [
|
||||
6208,
|
||||
1136
|
||||
],
|
||||
"id": "d458c7f0-f038-4bc1-8259-5581f6fe894b",
|
||||
"name": "Форма: Ошибка git",
|
||||
"webhookId": "b22c33d4-e55f-6677-b88c-99d00e11f222"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const fs = require('fs');\nconst path = require('path');\nconst localFolder = $('Подготовка переменных').first().json.localFolder;\n\nlet files;\ntry {\n files = fs.readdirSync(localFolder).filter(f => f.endsWith('.json'));\n} catch (e) {\n throw new Error(`Не удалось прочитать папку ${localFolder}: ${e.message}`);\n}\n\nif (files.length === 0) {\n throw new Error(`В папке ${localFolder} не найдено JSON файлов`);\n}\n\nconst allWorkflows = $input.all().map(item => item.json);\n\nconst results = [];\n\nfor (const fileName of files) {\n const filePath = path.join(localFolder, fileName);\n try {\n const raw = fs.readFileSync(filePath, 'utf8');\n const parsed = JSON.parse(raw);\n const wfData = Array.isArray(parsed) ? parsed[0] : parsed;\n\n if (!wfData || !wfData.name) {\n console.log(`SKIP: ${fileName} — нет поля name`);\n continue;\n }\n\n const existing = allWorkflows.find(wf => wf.name === wfData.name);\n\n let fileStatus;\n if (!existing) {\n fileStatus = 'NEW';\n } else {\n fileStatus = existing.updatedAt === wfData.updatedAt ? 'UNCHANGED' : 'CHANGED';\n }\n\n // Признак активации берём из файла\n const wfActive = !!(wfData.active);\n\n results.push({\n json: {\n fileName,\n filePath,\n fileStatus,\n workflowName: wfData.name,\n workflowData: wfData,\n existingId: existing ? existing.id : null,\n wfActive,\n }\n });\n\n console.log(`${fileStatus} [active=${wfActive}]: ${fileName}`);\n } catch (e) {\n console.log(`SKIP: ${fileName} — ошибка парсинга: ${e.message}`);\n }\n}\n\nif (results.length === 0) {\n throw new Error('Ни один файл не удалось распарсить');\n}\n\nconsole.log(`Прочитано файлов: ${results.length} из ${files.length}`);\nreturn results;"
|
||||
},
|
||||
"id": "4e566ce2-e54b-4c56-92ed-ff0eefbad2bc",
|
||||
"name": "Прочитать файлы из папки",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
6800,
|
||||
912
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "d56efecf-5f77-4cd8-a149-4f3fefdd0faa",
|
||||
"name": "Цикл по файлам",
|
||||
"type": "n8n-nodes-base.splitInBatches",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
7152,
|
||||
912
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const results = $input.all().map(item => item.json);\n\nconst total = results.length;\nconst created = results.filter(r => r.fileStatus === 'NEW' && r.imported).length;\nconst updated = results.filter(r => r.fileStatus === 'CHANGED' && r.imported).length;\nconst skipped = results.filter(r => r.fileStatus === 'UNCHANGED').length;\nconst failed = results.filter(r => r.fileStatus !== 'UNCHANGED' && !r.imported).length;\nconst activated = results.filter(r => r.activated).length;\n\nconst details = results.map(r => {\n if (r.fileStatus === 'UNCHANGED') return `➖ Пропущен: ${r.workflowName}`;\n const act = r.activated ? ' ▶️' : '';\n if (r.imported && r.fileStatus === 'NEW') return `✅ Создан: ${r.workflowName}${act}`;\n if (r.imported && r.fileStatus === 'CHANGED') return `🔄 Обновлён: ${r.workflowName}${act}`;\n return `❌ Ошибка: ${r.workflowName}`;\n}).join('\\n');\n\nconst activatedLine = activated > 0 ? ` | ▶️ Активировано: ${activated}` : '';\n\nreturn [{\n json: {\n total,\n created,\n updated,\n skipped,\n failed,\n activated,\n summary: `Всего: ${total} | Создано: ${created} | Обновлено: ${updated} | Пропущено: ${skipped} | Ошибок: ${failed}${activatedLine}`,\n details,\n }\n}];"
|
||||
},
|
||||
"id": "2ef6e478-b1b5-401f-8d37-d0c252e4c651",
|
||||
"name": "Сформировать сводку",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
8368,
|
||||
912
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "completion",
|
||||
"completionTitle": "Импорт завершён",
|
||||
"completionMessage": "={{ $json.summary }}\n\n{{ $json.details }}\n\n⚠️ Не забудьте перепривязать credentials в импортированных workflow.",
|
||||
"limitWaitTime": true,
|
||||
"resumeUnit": "minutes",
|
||||
"options": {
|
||||
"customCss": ":root {\n --font-size-body: 13px;\n --font-size-label: 13px;\n --font-size-test-notice: 12px;\n --font-size-input: 13px;\n --font-size-header: 20px;\n --font-size-paragraph: 13px;\n --font-size-link: 12px;\n --font-size-error: 12px;\n --font-size-html-h1: 28px;\n --font-size-html-h2: 20px;\n --font-size-html-h3: 16px;\n --font-size-html-h4: 13px;\n --font-size-html-h5: 12px;\n --font-size-html-h6: 11px;\n --font-size-subheader: 13px;\n --color-background: #1b1f23;\n --color-test-notice-text: #d29922;\n --color-test-notice-bg: #272115;\n --color-test-notice-border: #5a4010;\n --color-card-bg: #22272e;\n --color-card-border: #373e47;\n --color-card-shadow: rgba(0,0,0,0.4);\n --color-link: #539bf5;\n --color-html-link: #539bf5;\n --color-header: #cdd9e5;\n --color-header-subtext: #768390;\n --color-label: #adbac7;\n --color-input-border: #444c56;\n --color-input-text: #cdd9e5;\n --color-input-bg: #1c2128;\n --color-focus-border: #539bf5;\n --color-submit-btn-bg: #347d39;\n --color-submit-btn-text: #cdd9e5;\n --color-error: #e5534b;\n --color-required: #e5534b;\n --color-clear-button-bg: #636e7b;\n --color-html-text: #adbac7;\n --border-radius-card: 6px;\n --border-radius-input: 6px;\n --border-radius-clear-btn: 50%;\n --card-border-radius: 6px;\n --padding-container-top: 24px;\n --padding-card: 24px;\n --padding-test-notice-vertical: 10px;\n --padding-test-notice-horizontal: 16px;\n --margin-bottom-card: 16px;\n --padding-form-input: 12px;\n --card-padding: 24px;\n --card-margin-bottom: 16px;\n --container-width: 448px;\n --submit-btn-height: 44px;\n --checkbox-size: 16px;\n --box-shadow-card: 0px 4px 16px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.03);\n --opacity-placeholder: 0.35;\n}"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.form",
|
||||
"typeVersion": 2.5,
|
||||
"position": [
|
||||
8544,
|
||||
912
|
||||
],
|
||||
"id": "e615b2d1-d874-4288-9fac-ad4ba252674e",
|
||||
"name": "Форма: Импорт завершён",
|
||||
"webhookId": "c33d44e5-f66a-7788-c99d-00e11f2233g3"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=n8n import:workflow --input=\"{{ $json.filePath }}\"\necho \"IMPORTED: {{ $json.workflowName }}\""
|
||||
},
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
7760,
|
||||
976
|
||||
],
|
||||
"id": "87616442-e9c5-47fd-bc3a-0993b7f02408",
|
||||
"name": "Импортировать workflow"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const item = $input.first().json;\nconst stdout = item.stdout || '';\n\n// Парсим строки лога — каждая строка это JSON\nconst lines = stdout.split('\\n').filter(Boolean);\nconst logMessages = [];\n\nfor (const line of lines) {\n try {\n const parsed = JSON.parse(line);\n logMessages.push(parsed.message);\n } catch (e) {\n // не JSON строка — обычный текст (IMPORTED: ...)\n }\n}\n\nconst success = item.exitCode === 0;\nconst workflowName = stdout.match(/IMPORTED: (.+)/)?.[1]?.trim() || 'неизвестно';\n\nreturn [{\n json: {\n workflowName,\n success,\n message: logMessages.join(' → '),\n }\n}];"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
7184,
|
||||
1200
|
||||
],
|
||||
"id": "35a93ed9-385d-4372-9c97-3b0cf0e56598",
|
||||
"name": "Разбор результата импорта"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"filters": {},
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.n8n",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
6576,
|
||||
912
|
||||
],
|
||||
"id": "3695a508-3511-485d-b641-b8e8202c39fc",
|
||||
"name": "Получить все workflow n8n",
|
||||
"credentials": {
|
||||
"n8nApi": {
|
||||
"id": "aLPzwPxLHLLpPJIw",
|
||||
"name": "n8n local"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rules": {
|
||||
"values": [
|
||||
{
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict",
|
||||
"version": 3
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"leftValue": "={{ $json.fileStatus }}",
|
||||
"rightValue": "NEW",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
},
|
||||
"id": "e34cf093-cfc1-4ad6-98bd-6fa71911bd8c"
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"renameOutput": true,
|
||||
"outputKey": "NEW"
|
||||
},
|
||||
{
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict",
|
||||
"version": 3
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "5fc2aed6-0795-4d31-a763-13269a9d889c",
|
||||
"leftValue": "={{ $json.fileStatus }}",
|
||||
"rightValue": "CHANGED",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals",
|
||||
"name": "filter.operator.equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"renameOutput": true,
|
||||
"outputKey": "CHANGED"
|
||||
},
|
||||
{
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict",
|
||||
"version": 3
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "8b126f2d-d9bc-479a-b1ad-bdc7d429ca32",
|
||||
"leftValue": "={{ $json.fileStatus }}",
|
||||
"rightValue": "UNCHANGED",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals",
|
||||
"name": "filter.operator.equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"renameOutput": true,
|
||||
"outputKey": "UNCHANGED"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
7360,
|
||||
976
|
||||
],
|
||||
"id": "3d8141e0-fdb7-464e-8f9e-87cef96b0f37",
|
||||
"name": "Маршрутизация по статусу"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const fromSwitch = $('Маршрутизация по статусу').item.json;\nconst fromImport = $('Разбор результата импорта').item.json;\n\n// Активация через REST API: успех = нода вернула объект с active=true\nlet activated = false;\nlet activatedId = null;\ntry {\n const out = $('Активировать workflow').item.json;\n activated = out && out.active === true;\n activatedId = out && out.id ? out.id : null;\n} catch (e) {\n // Ветка FALSE у IF — ноду активации не проходили\n}\n\nreturn [{\n json: {\n fileName: fromSwitch.fileName,\n workflowName: fromSwitch.workflowName,\n fileStatus: fromSwitch.fileStatus,\n existingId: fromSwitch.existingId,\n activatedId,\n imported: fromImport.success,\n activated,\n message: fromImport.message,\n importedAt: new Date().toISOString(),\n }\n}];\n"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
7968,
|
||||
1200
|
||||
],
|
||||
"id": "08d1f524-a66f-4804-9d40-6dd98da463f8",
|
||||
"name": "Записать результат"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict",
|
||||
"version": 3
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "activate-global",
|
||||
"leftValue": "={{ $('Подготовка переменных').first().json.activate }}",
|
||||
"rightValue": true,
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "activate-wf-active",
|
||||
"leftValue": "={{ $('Маршрутизация по статусу').item.json.wfActive }}",
|
||||
"rightValue": true,
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "activate-success",
|
||||
"leftValue": "={{ $json.success }}",
|
||||
"rightValue": true,
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.3,
|
||||
"position": [
|
||||
7360,
|
||||
1200
|
||||
],
|
||||
"id": "c98aa5ee-1df4-441d-9648-539f841a3794",
|
||||
"name": "Нужно активировать?"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "activate",
|
||||
"workflowId": "={{ $json.id }}",
|
||||
"additionalFields": {},
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.n8n",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
7776,
|
||||
1200
|
||||
],
|
||||
"id": "8c91c94d-c6af-4249-a226-d280cb7692c4",
|
||||
"name": "Активировать workflow",
|
||||
"credentials": {
|
||||
"n8nApi": {
|
||||
"id": "aLPzwPxLHLLpPJIw",
|
||||
"name": "n8n local"
|
||||
}
|
||||
},
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const fromSwitch = $('Маршрутизация по статусу').item.json;\n\nreturn [{\n json: {\n fileName: fromSwitch.fileName,\n workflowName: fromSwitch.workflowName,\n fileStatus: 'UNCHANGED',\n existingId: fromSwitch.existingId,\n imported: false,\n activated: false,\n message: 'Пропущен — без изменений',\n importedAt: new Date().toISOString(),\n }\n}];"
|
||||
},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
7968,
|
||||
1024
|
||||
],
|
||||
"id": "1410b0c4-1ee9-4b22-b48a-017c8f362549",
|
||||
"name": "Записать результат (пропущен)"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"filters": {
|
||||
"name": "={{ $('Маршрутизация по статусу').item.json.workflowName }}"
|
||||
},
|
||||
"requestOptions": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.n8n",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
7568,
|
||||
1200
|
||||
],
|
||||
"id": "4baec163-2102-4114-982b-eba38a38beca",
|
||||
"name": "Найти id после импорта",
|
||||
"credentials": {
|
||||
"n8nApi": {
|
||||
"id": "aLPzwPxLHLLpPJIw",
|
||||
"name": "n8n local"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Форма запроса": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Подготовка переменных",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Ручной запуск (отладка)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Тестовые данные",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Тестовые данные": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Подготовка переменных",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Подготовка переменных": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Git Clone / Pull",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Git Clone / Pull": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Pull успешен?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Pull успешен?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Получить все workflow n8n",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Форма: Ошибка git",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Прочитать файлы из папки": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Цикл по файлам",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Цикл по файлам": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Сформировать сводку",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Маршрутизация по статусу",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Сформировать сводку": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Форма: Импорт завершён",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Импортировать workflow": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Разбор результата импорта",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Разбор результата импорта": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Нужно активировать?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Получить все workflow n8n": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Прочитать файлы из папки",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Маршрутизация по статусу": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Импортировать workflow",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Импортировать workflow",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Записать результат (пропущен)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Записать результат": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Цикл по файлам",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Нужно активировать?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Найти id после импорта",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Записать результат",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Активировать workflow": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Записать результат",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Записать результат (пропущен)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Цикл по файлам",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Найти id после импорта": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Активировать workflow",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"binaryMode": "separate"
|
||||
},
|
||||
"versionId": "cef6215b-db51-43c0-ac6c-65570a2c30dc",
|
||||
"meta": {
|
||||
"instanceId": "f720a996c27dff6663621d1fe2231dde9f00009fb064ea1db630bfc1dc798ddd"
|
||||
},
|
||||
"id": "0VqMxJC45kM28mgK",
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
FROM n8nio/runners:stable
|
||||
|
||||
USER root
|
||||
COPY requirements.txt /tmp/requirements.txt
|
||||
|
||||
# Установка Python пакетов через uv из requirements.txt
|
||||
RUN if [ -f /tmp/requirements.txt ]; then \
|
||||
echo "Installing Python packages from requirements.txt using uv..." && \
|
||||
echo "Contents:" && \
|
||||
cat /tmp/requirements.txt && \
|
||||
cd /opt/runners/task-runner-python && \
|
||||
uv pip install -r /tmp/requirements.txt && \
|
||||
echo "Python packages installed successfully"; \
|
||||
else \
|
||||
echo "No requirements.txt found, skipping Python packages installation"; \
|
||||
fi
|
||||
|
||||
# Установка JavaScript пакетов
|
||||
RUN cd /opt/runners/task-runner-javascript && \
|
||||
echo "Installing JS packages..." && \
|
||||
pnpm add moment uuid && \
|
||||
echo "JS packages installed successfully"
|
||||
|
||||
COPY n8n-task-runners.json /etc/n8n-task-runners.json
|
||||
|
||||
USER runner
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"task-runners": [
|
||||
{
|
||||
"runner-type": "javascript",
|
||||
"workdir": "/home/runner",
|
||||
"command": "/usr/local/bin/node",
|
||||
"args": [
|
||||
"--disallow-code-generation-from-strings",
|
||||
"--disable-proto=delete",
|
||||
"/opt/runners/task-runner-javascript/dist/start.js"
|
||||
],
|
||||
"health-check-server-port": "5681",
|
||||
"allowed-env": [
|
||||
"PATH",
|
||||
"GENERIC_TIMEZONE",
|
||||
"NODE_OPTIONS",
|
||||
"N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT",
|
||||
"N8N_RUNNERS_TASK_TIMEOUT",
|
||||
"N8N_RUNNERS_MAX_CONCURRENCY",
|
||||
"N8N_SENTRY_DSN",
|
||||
"ENVIRONMENT",
|
||||
"DEPLOYMENT_NAME",
|
||||
"HOME"
|
||||
],
|
||||
"env-overrides": {
|
||||
"NODE_FUNCTION_ALLOW_BUILTIN": "*",
|
||||
"NODE_FUNCTION_ALLOW_EXTERNAL": "*",
|
||||
"NODE_PATH": "/home/runner/node_modules",
|
||||
"N8N_RUNNERS_HEALTH_CHECK_SERVER_HOST": "0.0.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"runner-type": "python",
|
||||
"workdir": "/home/runner",
|
||||
"command": "/opt/runners/task-runner-python/.venv/bin/python",
|
||||
"args": ["-m", "src.main"],
|
||||
"health-check-server-port": "5682",
|
||||
"allowed-env": [
|
||||
"PATH",
|
||||
"GENERIC_TIMEZONE",
|
||||
"N8N_RUNNERS_LAUNCHER_LOG_LEVEL",
|
||||
"N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT",
|
||||
"N8N_RUNNERS_TASK_TIMEOUT",
|
||||
"N8N_RUNNERS_MAX_CONCURRENCY",
|
||||
"N8N_SENTRY_DSN",
|
||||
"ENVIRONMENT",
|
||||
"DEPLOYMENT_NAME"
|
||||
],
|
||||
"env-overrides": {
|
||||
"PYTHONPATH": "/opt/runners/task-runner-python",
|
||||
"N8N_RUNNERS_STDLIB_ALLOW": "*",
|
||||
"N8N_RUNNERS_EXTERNAL_ALLOW": "*"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
requests
|
||||
@@ -0,0 +1,7 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
|
||||
scrape_configs:
|
||||
- job_name: "n8n"
|
||||
static_configs:
|
||||
- targets: ["n8n:5678"]
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/bin/sh
|
||||
# Import n8n workflows and credentials from backup directory
|
||||
# This script runs inside the n8n-import container
|
||||
#
|
||||
# Credentials skip logic:
|
||||
# После успешного импорта создаётся маркер в /home/node/.n8n/imported/
|
||||
# При повторном запуске файл с существующим маркером пропускается.
|
||||
# Это позволяет не перезатирать API-ключи при повторном деплое.
|
||||
# Чтобы принудительно переимпортировать — удалить маркер или задать FORCE_IMPORT=true
|
||||
|
||||
set -e
|
||||
|
||||
MARKER_DIR="/home/node/.n8n/imported"
|
||||
mkdir -p "$MARKER_DIR"
|
||||
|
||||
COUNTER_FILE=$(mktemp)
|
||||
trap 'rm -f "$COUNTER_FILE"' EXIT
|
||||
echo "0" > "$COUNTER_FILE"
|
||||
|
||||
# Import credentials first
|
||||
echo 'Importing credentials...'
|
||||
CRED_FILES=$(find /backup/credentials -maxdepth 1 -type f -not -name '.gitkeep' 2>/dev/null || true)
|
||||
if [ -n "$CRED_FILES" ]; then
|
||||
CRED_COUNT=$(echo "$CRED_FILES" | wc -l | tr -d ' ')
|
||||
|
||||
# Normalize credentials files (wrap object in array if needed)
|
||||
echo "$CRED_FILES" | while IFS= read -r file; do
|
||||
FIRST_CHAR=$(head -c 1 "$file")
|
||||
if [ "$FIRST_CHAR" = "{" ]; then
|
||||
TMPFILE=$(mktemp)
|
||||
echo "[$(cat "$file")]" > "$TMPFILE"
|
||||
mv "$TMPFILE" "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "0" > "$COUNTER_FILE"
|
||||
echo "$CRED_FILES" | while IFS= read -r file; do
|
||||
CURRENT=$(cat "$COUNTER_FILE")
|
||||
CURRENT=$((CURRENT + 1))
|
||||
echo "$CURRENT" > "$COUNTER_FILE"
|
||||
filename=$(basename "$file")
|
||||
MARKER="$MARKER_DIR/cred_$(echo "$filename" | tr '/' '_')"
|
||||
|
||||
printf "[%2d/%d] %s" "$CURRENT" "$CRED_COUNT" "$filename"
|
||||
|
||||
# Пропустить если уже импортировался и FORCE_IMPORT не задан
|
||||
if [ -f "$MARKER" ] && [ "$FORCE_IMPORT" != "true" ]; then
|
||||
echo " SKIPPED (already imported, set FORCE_IMPORT=true to override)"
|
||||
continue
|
||||
fi
|
||||
|
||||
OUTPUT_FILE=$(mktemp)
|
||||
|
||||
if n8n import:credentials --input="$file" > "$OUTPUT_FILE" 2>&1; then
|
||||
echo " OK"
|
||||
touch "$MARKER"
|
||||
if [ -s "$OUTPUT_FILE" ]; then
|
||||
echo " Details: $(cat "$OUTPUT_FILE" | tr '\n' ' ')"
|
||||
fi
|
||||
else
|
||||
echo " FAILED"
|
||||
echo " Error: $(cat "$OUTPUT_FILE" | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
rm -f "$OUTPUT_FILE"
|
||||
done
|
||||
fi
|
||||
|
||||
# Import workflows
|
||||
echo ''
|
||||
echo 'Importing workflows...'
|
||||
WORKFLOW_FILES=$(find /backup/workflows -maxdepth 1 -type f -not -name '.gitkeep' 2>/dev/null || true)
|
||||
if [ -z "$WORKFLOW_FILES" ]; then
|
||||
echo 'No workflows found to import.'
|
||||
exit 0
|
||||
fi
|
||||
TOTAL_FOUND=$(echo "$WORKFLOW_FILES" | wc -l | tr -d ' ')
|
||||
if [ -n "$IMPORT_LIMIT" ]; then
|
||||
WORKFLOW_FILES=$(echo "$WORKFLOW_FILES" | head -n "$IMPORT_LIMIT")
|
||||
fi
|
||||
TOTAL=$(echo "$WORKFLOW_FILES" | wc -l | tr -d ' ')
|
||||
echo "Importing $TOTAL of $TOTAL_FOUND workflows"
|
||||
echo ''
|
||||
echo "0" > "$COUNTER_FILE"
|
||||
echo "$WORKFLOW_FILES" | while IFS= read -r file; do
|
||||
CURRENT=$(cat "$COUNTER_FILE")
|
||||
CURRENT=$((CURRENT + 1))
|
||||
echo "$CURRENT" > "$COUNTER_FILE"
|
||||
filename=$(basename "$file")
|
||||
printf "[%3d/%d] %s" "$CURRENT" "$TOTAL" "$filename"
|
||||
|
||||
OUTPUT_FILE=$(mktemp)
|
||||
|
||||
if n8n import:workflow --input="$file" > "$OUTPUT_FILE" 2>&1; then
|
||||
echo " OK"
|
||||
if [ -s "$OUTPUT_FILE" ]; then
|
||||
OUTPUT=$(cat "$OUTPUT_FILE")
|
||||
if echo "$OUTPUT" | grep -q "id"; then
|
||||
echo " Imported: $(echo "$OUTPUT" | tr '\n' ' ' | sed 's/[{}"]//g')"
|
||||
else
|
||||
echo " Details: $OUTPUT"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo " FAILED"
|
||||
echo " Error: $(cat "$OUTPUT_FILE" | tr '\n' ' ')"
|
||||
fi
|
||||
|
||||
rm -f "$OUTPUT_FILE"
|
||||
done
|
||||
echo ''
|
||||
echo 'Import complete!'
|
||||
Reference in New Issue
Block a user