Кирилл

Вечкасов

Статьи
Наши курсы

Кирилл

Вечкасов

>

>

Обнолвение n8n в Easy Panel

Скрипт автоматического обновления n8n в Docker Swarm

Я активно использую n8n через Easypanel для автоматизации бизнес-процессов. Столкнулся с проблемой: после каждого обновления контейнера все сохранённые credentials (API ключи, токены доступа, пароли от сервисов) слетали. Приходилось вручную заходить и заново настраивать все подключения к Telegram, Google Sheets, базам данных и другим сервисам.
Обновлять n8n нужно было регулярно - выходят новые фичи, исправления безопасности, но каждый раз тратить 20-30 минут на восстановление credentials было неприемлемо.

Покопавшись в проблеме, понял что дело в отсутствии постоянного ключа шифрования (N8N_ENCRYPTION_KEY). При пересоздании контейнера генерировался новый ключ, и старые зашифрованные данные становились нечитаемыми.

Решил автоматизировать процесс: написал bash-скрипт, который не только обновляет n8n, но и проверяет защиту данных перед обновлением. Бонусом добавил отправку уведомлений в Telegram - теперь вижу когда и что обновилось, не заходя на сервер.

Скрипт работает уже несколько месяцев в продакшене, обновляет два экземпляра n8n каждую ночь. Ни одного слёта credentials. Делюсь решением - возможно, кому-то пригодится.

Установка

1. Создать файл

nano /usr/local/bin/n8n_update.sh

Вставить код скрипта (расположенный ниже), сохраните. Или скопирайте код, создайте новый текстовый файл вручную и разместите его тут "/usr/local/bin/"

2. Выдайте права на выполнение

chmod +x /usr/local/bin/n8n_update.sh

 3. Добавьте в автозапуск данный файл

(crontab -l 2>/dev/null; echo "0 0 * * * /usr/local/bin/n8n_update.sh") | crontab -

Скрипт для установки

Если требуется отправка уведомлений в личную ТГ группы, то в SEND_TO_TG выставьте true, а вместо "***" вставьте Токен своего бота и ИД чата куда отправлять сообщения об обновлении.

#!/bin/bash

# Настройки
export TZ=Europe/Moscow
IMAGE_NAME="n8nio/n8n:latest"
BOT_TOKEN="8008729465:AAGGcX4QXJ1ogb4MFCGyNrxP5-OnSPOYZDk"
CHAT_ID="-1002266062656"
SEND_TO_TG=true
LOG_FILE="/var/log/n8n_update.log"
SCRIPT_PATH="/usr/local/bin/n8n_update.sh"
VERSION_FILE="/var/log/n8n_version.txt"

# Функция для отправки в Telegram
send_telegram() {
    local message="$1"
    if [ "$SEND_TO_TG" = true ]; then
        curl -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
             -d "chat_id=${CHAT_ID}" \
             -d "text=${message}" \
             -d "parse_mode=HTML" > /dev/null 2>&1
    fi
}

# Функция для логирования ошибок
log_error() {
    local error_message="$1"
    echo "$(date '+%Y-%m-%d %H:%M:%S') - ОШИБКА: ${error_message}" >> $LOG_FILE
    send_telegram "❌ [n8n ОШИБКА] ${error_message}"
}

# Функция для получения версии n8n из образа
get_n8n_version() {
    local image="$1"
    docker run --rm --entrypoint="" "$image" n8n --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -n1
}

# Начало работы скрипта
echo "========================================" >> $LOG_FILE
echo "$(date '+%Y-%m-%d %H:%M:%S') - Скрипт запущен" >> $LOG_FILE

# Проверка Docker
if ! command -v docker &> /dev/null; then
    log_error "Docker не найден в системе"
    exit 1
fi

if ! docker info > /dev/null 2>&1; then
    log_error "Нет доступа к Docker"
    exit 1
fi

# Получаем текущую версию из локального образа
CURRENT_VERSION=$(get_n8n_version "$IMAGE_NAME")
echo "$(date '+%Y-%m-%d %H:%M:%S') - Текущая версия образа: $CURRENT_VERSION" >> $LOG_FILE

# Сохраняем предыдущую версию
PREVIOUS_VERSION=""
if [ -f "$VERSION_FILE" ]; then
    PREVIOUS_VERSION=$(cat "$VERSION_FILE")
    echo "$(date '+%Y-%m-%d %H:%M:%S') - Предыдущая версия: $PREVIOUS_VERSION" >> $LOG_FILE
fi

# Получаем локальный digest базового образа
LOCAL_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $IMAGE_NAME 2>/dev/null || echo "none")
echo "$(date '+%Y-%m-%d %H:%M:%S') - Локальный digest: $LOCAL_DIGEST" >> $LOG_FILE

# Получаем digest latest с Docker Hub API
REMOTE_DIGEST=$(curl -s "https://registry.hub.docker.com/v2/repositories/n8nio/n8n/tags/latest" \
    | grep -o '"digest":"[^"]*' | head -n1 | cut -d'"' -f4)

if [ -z "$REMOTE_DIGEST" ]; then
    log_error "Не удалось получить удалённый digest с Docker Hub"
    exit 1
fi

echo "$(date '+%Y-%m-%d %H:%M:%S') - Удалённый digest: $REMOTE_DIGEST" >> $LOG_FILE

BASE_IMAGE_UPDATED=false
VERSION_CHANGED=false
NEW_VERSION=""

if [ "$LOCAL_DIGEST" != "n8nio/n8n@$REMOTE_DIGEST" ]; then
    echo "$(date '+%Y-%m-%d %H:%M:%S') - Найдено обновление digest образа!" >> $LOG_FILE
    if ! docker pull $IMAGE_NAME >> $LOG_FILE 2>&1; then
        log_error "Не удалось загрузить новый образ $IMAGE_NAME"
        exit 1
    fi
    
    # Получаем версию нового образа
    NEW_VERSION=$(get_n8n_version "$IMAGE_NAME")
    echo "$(date '+%Y-%m-%d %H:%M:%S') - Версия в новом образе: $NEW_VERSION" >> $LOG_FILE
    
    # Проверяем, изменилась ли версия n8n
    if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
        BASE_IMAGE_UPDATED=true
        VERSION_CHANGED=true
        echo "$(date '+%Y-%m-%d %H:%M:%S') - Обнаружена новая версия n8n: $CURRENT_VERSION → $NEW_VERSION" >> $LOG_FILE
        # Сохраняем новую версию
        echo "$NEW_VERSION" > "$VERSION_FILE"
    else
        echo "$(date '+%Y-%m-%d %H:%M:%S') - Digest изменился, но версия n8n осталась прежней ($CURRENT_VERSION)" >> $LOG_FILE
    fi
else
    echo "$(date '+%Y-%m-%d %H:%M:%S') - Образ уже актуален" >> $LOG_FILE
fi

# Находим все сервисы n8n
SERVICES=$(docker service ls --format '{{.Name}}' | grep n8n)

if [ -z "$SERVICES" ]; then
    send_telegram "⚠️ [n8n] Сервисы n8n не найдены"
    echo "$(date '+%Y-%m-%d %H:%M:%S') - Сервисы n8n не найдены" >> $LOG_FILE
    exit 0
fi

UPDATED_COUNT=0
TOTAL_COUNT=0
MESSAGE=""

# Формируем заголовок сообщения
if [ "$BASE_IMAGE_UPDATED" = true ] && [ -n "$NEW_VERSION" ]; then
    if [ -n "$PREVIOUS_VERSION" ] && [ "$PREVIOUS_VERSION" != "$NEW_VERSION" ]; then
        MESSAGE="🔄 [n8n Update] ${PREVIOUS_VERSION} → ${NEW_VERSION}%0A%0A"
    else
        MESSAGE="🔄 [n8n Update] Обновление до версии ${NEW_VERSION}%0A%0A"
    fi
else
    MESSAGE="🔄 [n8n Update] Проверка сервисов...%0A%0A"
fi

# Обрабатываем каждый сервис
for SERVICE_NAME in $SERVICES; do
    TOTAL_COUNT=$((TOTAL_COUNT + 1))
    echo "$(date '+%Y-%m-%d %H:%M:%S') - Проверка сервиса: $SERVICE_NAME" >> $LOG_FILE
    
    if [ "$BASE_IMAGE_UPDATED" = true ]; then
        if docker service update --image $IMAGE_NAME --with-registry-auth --update-order stop-first "$SERVICE_NAME" >> $LOG_FILE 2>&1; then
            MESSAGE="${MESSAGE}✅ ${SERVICE_NAME} обновлён%0A"
            echo "$(date '+%Y-%m-%d %H:%M:%S') - Сервис обновлён" >> $LOG_FILE
            UPDATED_COUNT=$((UPDATED_COUNT + 1))
        else
            MESSAGE="${MESSAGE}❌ ${SERVICE_NAME} ошибка обновления%0A"
            echo "$(date '+%Y-%m-%d %H:%M:%S') - Ошибка обновления" >> $LOG_FILE
        fi
    else
        MESSAGE="${MESSAGE}⏭️ ${SERVICE_NAME} актуален%0A"
    fi
done

# Итоговое сообщение
if [ $UPDATED_COUNT -gt 0 ]; then
    MESSAGE="${MESSAGE}%0A📊 Обновлено: ${UPDATED_COUNT}/${TOTAL_COUNT}"
else
    if [ -n "$CURRENT_VERSION" ]; then
        MESSAGE="✅ [n8n] Все сервисы актуальны%0A%0A📌 Версия: ${CURRENT_VERSION}%0A📊 Проверено: ${TOTAL_COUNT}"
    else
        MESSAGE="✅ [n8n] Все сервисы актуальны%0A%0A📊 Проверено: ${TOTAL_COUNT}"
    fi
fi

send_telegram "$MESSAGE"

# Очистка старых образов
echo "$(date '+%Y-%m-%d %H:%M:%S') - Очистка старых образов..." >> $LOG_FILE
OLD_IMAGES=$(docker images "n8nio/n8n" --format "{{.ID}} {{.Repository}}:{{.Tag}}" | grep -v "latest" | awk '{print $1}')

if [ -n "$OLD_IMAGES" ]; then
    for IMAGE_ID in $OLD_IMAGES; do
        if ! docker ps -a --filter "ancestor=$IMAGE_ID" --format "{{.ID}}" | grep -q .; then
            docker rmi $IMAGE_ID >> $LOG_FILE 2>&1
            echo "$(date '+%Y-%m-%d %H:%M:%S') - Удалён старый образ: $IMAGE_ID" >> $LOG_FILE
        fi
    done
fi

docker image prune -f >> $LOG_FILE 2>&1

echo "$(date '+%Y-%m-%d %H:%M:%S') - Скрипт завершён" >> $LOG_FILE
echo "========================================" >> $LOG_FILE

Мои соц. сети

Подписывайся на мой Telegram-канал — там регулярно публикую фишки по n8n, кейсы по рекламе и практичные решения для бизнеса.