Я активно использую 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