June 5

Автоматическое резервное копирование директории /root/node на удалённый сервер

+ в скрипт: docker exec -it node-wg-1 iptables -t mangle -A FORWARD -o wg0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

+ в скрипт: ethtool -K ens3 tso off gso off gro off

Самый удобный вариант — не редактировать руками скрипт, а сделать установочный скрипт, который принимает параметры, например так:

bash setup-node-backup.sh austria-2 10.10.10.10

Где:

  • austria-2 — это имя сервера, которое пойдёт в путь и в имя архива
  • 10.10.10.10 — IP резервного сервера

Ниже готовый вариант.


1.1. Скрипт установки ноды

Создай файл:

nano setup-node-backup.sh

И вставь туда:

#!/bin/bash
set -euo pipefail

SERVER_NAME="${1:-}"
REMOTE_HOST="${2:-}"

if [[ -z "$SERVER_NAME" || -z "$REMOTE_HOST" ]]; then
    echo "Использование: bash setup-node-backup.sh <server_name> <reserve_ip>"
    echo "Пример: bash setup-node-backup.sh austria-2 192.168.1.10"
    exit 1
fi

WATCH_DIR="/root/node"
LOCAL_BACKUP_DIR="/root/node_backups"
SCRIPT_PATH="/usr/local/bin/node-backup-watcher.sh"
SERVICE_PATH="/etc/systemd/system/node-backup-watcher.service"
REMOTE_USER="root"
REMOTE_DIR="/root/backup_node/${SERVER_NAME}"

if [[ $EUID -ne 0 ]]; then
    echo "Ошибка: запустите скрипт от root"
    exit 1
fi

echo "[1/7] Устанавливаем зависимости..."
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y inotify-tools zip openssh-client

echo "[2/7] Проверяем директории..."
mkdir -p "$LOCAL_BACKUP_DIR"
if [[ ! -d "$WATCH_DIR" ]]; then
    echo "Ошибка: директория $WATCH_DIR не существует"
    exit 1
fi

echo "[3/7] Проверяем SSH-ключ..."
install -d -m 700 /root/.ssh
if [[ ! -f /root/.ssh/id_rsa ]]; then
    ssh-keygen -t rsa -b 4096 -N "" -f /root/.ssh/id_rsa
fi

echo "[3.1/7] Устанавливаем SSH-доступ на удалённый сервер..."
ssh-copy-id -o StrictHostKeyChecking=accept-new "${REMOTE_USER}@${REMOTE_HOST}"

echo "[3.2/7] Проверяем SSH-подключение..."
ssh \
    -o BatchMode=yes \
    -o ConnectTimeout=10 \
    -o ConnectionAttempts=3 \
    -o ServerAliveInterval=10 \
    -o ServerAliveCountMax=3 \
    -o StrictHostKeyChecking=accept-new \
    "${REMOTE_USER}@${REMOTE_HOST}" "echo SSH_OK" >/dev/null

echo "[4/7] Создаём watcher..."
cat > "$SCRIPT_PATH" <<EOF
#!/bin/bash
set -uo pipefail

WATCH_DIR="${WATCH_DIR}"
LOCAL_BACKUP_DIR="${LOCAL_BACKUP_DIR}"
REMOTE_USER="${REMOTE_USER}"
REMOTE_HOST="${REMOTE_HOST}"
REMOTE_DIR="${REMOTE_DIR}"
SERVER_NAME="${SERVER_NAME}"

SSH_OPTIONS=(
    -o BatchMode=yes
    -o ConnectTimeout=10
    -o ConnectionAttempts=3
    -o ServerAliveInterval=10
    -o ServerAliveCountMax=3
    -o StrictHostKeyChecking=accept-new
)

log() {
    printf '%s %s\\n' "\$(date '+%Y-%m-%d %H:%M:%S')" "\$*"
}

retry() {
    local attempt max_attempts=3 delay=10
    for ((attempt=1; attempt<=max_attempts; attempt++)); do
        if "\$@"; then
            return 0
        fi
        log "Ошибка команды, попытка \${attempt}/\${max_attempts}"
        if (( attempt < max_attempts )); then
            sleep "\$delay"
        fi
    done
    return 1
}

upload_archive() {
    local archive_path="\$1"
    local archive_name
    archive_name="\$(basename "\$archive_path")"

    retry ssh "\${SSH_OPTIONS[@]}" "\${REMOTE_USER}@\${REMOTE_HOST}" \
        "mkdir -p '\${REMOTE_DIR}'" </dev/null || return 1

    retry scp "\${SSH_OPTIONS[@]}" "\$archive_path" \
        "\${REMOTE_USER}@\${REMOTE_HOST}:\${REMOTE_DIR}/\${archive_name}" \
        </dev/null || return 1

    touch "\${archive_path}.uploaded"
    log "Архив отправлен: \$archive_name"
}

send_pending_archives() {
    local archive
    while IFS= read -r -d '' archive; do
        [[ -f "\${archive}.uploaded" ]] && continue
        log "Отправляем: \$(basename "\$archive")"
        if ! upload_archive "\$archive"; then
            log "Не удалось отправить архив; он сохранён локально для следующей попытки"
            return 1
        fi
    done < <(find "\$LOCAL_BACKUP_DIR" -maxdepth 1 -type f \
        -name "\${SERVER_NAME}-backup_*.zip" -print0 | sort -z)
}

cleanup_local_archives() {
    local archive
    local -a archives=()

    while IFS= read -r -d '' archive; do
        archives+=("\$archive")
    done < <(find "\$LOCAL_BACKUP_DIR" -maxdepth 1 -type f \
        -name "\${SERVER_NAME}-backup_*.zip" -printf '%T@ %p\\0' \
        | sort -zrn | cut -z -d ' ' -f 2-)

    if (( \${#archives[@]} <= 3 )); then
        return 0
    fi

    for archive in "\${archives[@]:3}"; do
        rm -f -- "\$archive" "\${archive}.uploaded"
        log "Удалён старый локальный архив: \$(basename "\$archive")"
    done
}

create_backup() {
    local timestamp archive_name archive_path partial_path
    timestamp="\$(date +'%Y-%m-%d_%H-%M-%S')"
    archive_name="\${SERVER_NAME}-backup_\${timestamp}.zip"
    archive_path="\${LOCAL_BACKUP_DIR}/\${archive_name}"
    partial_path="\${archive_path%.zip}.partial.zip"

    log "Создаём архив: \$archive_path"
    rm -f -- "\$partial_path"
    if ! (cd "\$(dirname "\$WATCH_DIR")" && \
        zip -r -q "\$partial_path" "\$(basename "\$WATCH_DIR")"); then
        rm -f -- "\$partial_path"
        log "Ошибка создания архива"
        return 1
    fi
    mv -f -- "\$partial_path" "\$archive_path"

    if send_pending_archives; then
        cleanup_local_archives
        log "Бэкап завершён: \$archive_name"
        return 0
    fi

    cleanup_local_archives
    return 1
}

mkdir -p "\$LOCAL_BACKUP_DIR"
if [[ ! -d "\$WATCH_DIR" ]]; then
    log "Ошибка: директория \$WATCH_DIR не существует"
    exit 1
fi

log "Watcher запущен. Отслеживаем: \$WATCH_DIR"

while IFS= read -r event; do
    log "Обнаружено изменение: \$event"

    # Объединяем серию частых событий в один бэкап.
    sleep 5
    while IFS= read -r -t 0.2 newer_event; do
        event="\$newer_event"
    done

    if ! create_backup; then
        log "Бэкап завершился ошибкой; watcher продолжает работу"
    fi
done < <(inotifywait -m -r -e modify,create,delete,move \
    "\$WATCH_DIR" --format '%w%f %e')

log "inotifywait неожиданно завершился"
exit 1
EOF

chmod 700 "$SCRIPT_PATH"

echo "[5/7] Создаём systemd-сервис..."
cat > "$SERVICE_PATH" <<EOF
[Unit]
Description=Node Folder Backup Watcher
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=${SCRIPT_PATH}
Restart=always
RestartSec=5
User=root
KillMode=control-group
TimeoutStopSec=20

[Install]
WantedBy=multi-user.target
EOF

echo "[6/7] Создаём папку на резервном сервере..."
ssh -o BatchMode=yes -o ConnectTimeout=10 -o ConnectionAttempts=3 \
    -o StrictHostKeyChecking=accept-new \
    "${REMOTE_USER}@${REMOTE_HOST}" "mkdir -p '${REMOTE_DIR}'"

echo "[7/7] Активируем сервис..."
systemctl daemon-reload
systemctl enable node-backup-watcher.service
systemctl restart node-backup-watcher.service

echo
echo "Готово."
echo "Имя сервера: ${SERVER_NAME}"
echo "Резервный сервер: ${REMOTE_HOST}"
echo "Удалённая директория: ${REMOTE_DIR}"
echo
systemctl --no-pager --full status node-backup-watcher.service

Сохрани и сделай исполняемым:

chmod +x setup-node-backup.sh

1.2. Также, что касается скрипта бэкапа tower

Создай файл:

nano setup-tower-backup.sh

И вставь туда:

#!/usr/bin/env bash
set -Eeuo pipefail

SERVER_NAME="${1:-}"
REMOTE_HOST="${2:-}"

if [[ -z "$SERVER_NAME" || -z "$REMOTE_HOST" ]]; then
    echo "Использование: bash setup-tower-backup.sh <server_name> <reserve_ip>"
    echo "Пример: bash setup-tower-backup.sh tower 5.35.87.8"
    exit 1
fi

if [[ ! "$SERVER_NAME" =~ ^[a-zA-Z0-9._-]+$ ]]; then
    echo "Ошибка: некорректное имя сервера: $SERVER_NAME"
    exit 1
fi

if [[ ! "$REMOTE_HOST" =~ ^[a-zA-Z0-9.:-]+$ ]]; then
    echo "Ошибка: некорректный адрес: $REMOTE_HOST"
    exit 1
fi

WATCH_DIR="/root/tower"
LOCAL_BACKUP_DIR="/root/tower_backups"
SCRIPT_PATH="/usr/local/bin/tower-backup-watcher.sh"
CONFIG_PATH="/etc/default/tower-backup-watcher"
SERVICE_PATH="/etc/systemd/system/tower-backup-watcher.service"

REMOTE_USER="root"
REMOTE_DIR="/root/backup_tower/${SERVER_NAME}"

DEBOUNCE_SECONDS=15
KEEP_LOCAL=3

echo "[1/8] Проверяем директорию Tower..."

if [[ ! -d "$WATCH_DIR" ]]; then
    echo "Ошибка: папка $WATCH_DIR не существует"
    exit 1
fi

echo "[2/8] Устанавливаем зависимости..."

export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y inotify-tools zip unzip openssh-client

echo "[3/8] Создаём локальную директорию..."

install -d -m 700 "$LOCAL_BACKUP_DIR"
install -d -m 700 /root/.ssh

echo "[4/8] Настраиваем SSH-ключ..."

if [[ -f /root/.ssh/id_ed25519.pub ]]; then
    PUBLIC_KEY="/root/.ssh/id_ed25519.pub"
elif [[ -f /root/.ssh/id_rsa.pub ]]; then
    PUBLIC_KEY="/root/.ssh/id_rsa.pub"
else
    ssh-keygen \
        -t ed25519 \
        -N "" \
        -f /root/.ssh/id_ed25519

    PUBLIC_KEY="/root/.ssh/id_ed25519.pub"
fi

echo
echo "Сейчас потребуется пароль root от резервного сервера:"
ssh-copy-id -i "$PUBLIC_KEY" "${REMOTE_USER}@${REMOTE_HOST}"

ssh \
    -o BatchMode=yes \
    -o ConnectTimeout=10 \
    "${REMOTE_USER}@${REMOTE_HOST}" \
    "mkdir -p '$REMOTE_DIR' && echo SSH_OK"

echo "[5/8] Создаём конфигурацию..."

cat > "$CONFIG_PATH" <<EOF
SERVER_NAME=$SERVER_NAME
WATCH_DIR=$WATCH_DIR
LOCAL_BACKUP_DIR=$LOCAL_BACKUP_DIR

REMOTE_USER=$REMOTE_USER
REMOTE_HOST=$REMOTE_HOST
REMOTE_DIR=$REMOTE_DIR

DEBOUNCE_SECONDS=$DEBOUNCE_SECONDS
KEEP_LOCAL=$KEEP_LOCAL
EOF

chmod 600 "$CONFIG_PATH"

echo "[6/8] Создаём watcher..."

cat > "$SCRIPT_PATH" <<'WATCHER'
#!/usr/bin/env bash
set -Eeuo pipefail

: "${SERVER_NAME:?}"
: "${WATCH_DIR:?}"
: "${LOCAL_BACKUP_DIR:?}"
: "${REMOTE_USER:?}"
: "${REMOTE_HOST:?}"
: "${REMOTE_DIR:?}"

DEBOUNCE_SECONDS="${DEBOUNCE_SECONDS:-15}"
KEEP_LOCAL="${KEEP_LOCAL:-3}"

EVENT_FILE="/run/tower-backup-watcher.pending"
PROCESSING_FILE="/run/tower-backup-watcher.processing"

SSH_OPTIONS=(
    -o BatchMode=yes
    -o ConnectTimeout=10
    -o ServerAliveInterval=15
    -o ServerAliveCountMax=3
    -o StrictHostKeyChecking=yes
)

log()
{
    printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}

cleanup_local_archives()
{
    local -a archives=()
    local index
    local archive

    mapfile -d '' -t archives < <(
        find "$LOCAL_BACKUP_DIR" \
            -maxdepth 1 \
            -type f \
            -name '*.zip' \
            -printf '%T@ %p\0' |
        sort -z -nr
    )

    for ((index=KEEP_LOCAL; index<${#archives[@]}; index++)); do
        archive="${archives[$index]#* }"
        rm -- "$archive"
        log "Удалён старый локальный архив: $archive"
    done
}

create_and_upload_backup()
{
    local timestamp
    local archive_name
    local archive_path
    local watch_parent
    local watch_name

    timestamp="$(date '+%Y-%m-%d_%H-%M-%S')"

    # При SERVER_NAME=tower получится:
    # tower-backup_2026-08-13_14-28-07.zip
    archive_name="${SERVER_NAME}-backup_${timestamp}.zip"
    archive_path="${LOCAL_BACKUP_DIR}/${archive_name}"

    watch_parent="$(dirname "$WATCH_DIR")"
    watch_name="$(basename "$WATCH_DIR")"

    log "Создаём архив: $archive_path"

    if ! (
        cd "$watch_parent"
        zip -r -q "$archive_path" "$watch_name"
    ); then
        rm -f -- "$archive_path"
        log "ОШИБКА: не удалось создать архив"
        return 1
    fi

    if ! unzip -tqq "$archive_path"; then
        log "ОШИБКА: ZIP не прошёл проверку: $archive_path"
        return 1
    fi

    if ! ssh "${SSH_OPTIONS[@]}" \
        "${REMOTE_USER}@${REMOTE_HOST}" \
        "mkdir -p '$REMOTE_DIR'"
    then
        log "ОШИБКА: резервный сервер недоступен"
        return 1
    fi

    if ! scp "${SSH_OPTIONS[@]}" \
        "$archive_path" \
        "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"
    then
        log "ОШИБКА: не удалось отправить $archive_name"
        return 1
    fi

    log "Архив отправлен: ${REMOTE_HOST}:${REMOTE_DIR}/${archive_name}"

    cleanup_local_archives
}

watch_events()
{
    inotifywait \
        -q \
        -m \
        -r \
        -e close_write,create,delete,moved_to,moved_from \
        --format '%w%f' \
        "$WATCH_DIR" |
    while IFS= read -r changed_path; do
        date '+%s' > "${EVENT_FILE}.tmp"
        mv -f "${EVENT_FILE}.tmp" "$EVENT_FILE"
        log "Обнаружено изменение: $changed_path"
    done
}

mkdir -p "$LOCAL_BACKUP_DIR"
rm -f "$EVENT_FILE" "$PROCESSING_FILE" "${EVENT_FILE}.tmp"

watch_events &
MONITOR_PID=$!

cleanup()
{
    kill "$MONITOR_PID" 2>/dev/null || true
    rm -f "$EVENT_FILE" "$PROCESSING_FILE" "${EVENT_FILE}.tmp"
}

trap cleanup EXIT INT TERM

log "Watcher запущен для $WATCH_DIR"
log "Debounce: ${DEBOUNCE_SECONDS} секунд"

while kill -0 "$MONITOR_PID" 2>/dev/null; do
    if [[ -s "$EVENT_FILE" ]]; then
        last_event="$(<"$EVENT_FILE")"
        current_time="$(date '+%s')"

        if [[ "$last_event" =~ ^[0-9]+$ ]] &&
           ((current_time - last_event >= DEBOUNCE_SECONDS))
        then
            if mv "$EVENT_FILE" "$PROCESSING_FILE" 2>/dev/null; then
                claimed_event="$(<"$PROCESSING_FILE")"
                current_time="$(date '+%s')"

                # Повторно проверяем timestamp, чтобы не схватить
                # событие, обновлённое прямо во время mv.
                if ((current_time - claimed_event < DEBOUNCE_SECONDS)); then
                    if [[ -e "$EVENT_FILE" ]]; then
                        rm -f "$PROCESSING_FILE"
                    else
                        mv "$PROCESSING_FILE" "$EVENT_FILE"
                    fi
                else
                    rm -f "$PROCESSING_FILE"

                    if ! create_and_upload_backup; then
                        log "Повторим отправку после debounce"

                        date '+%s' > "${EVENT_FILE}.tmp"
                        mv -f "${EVENT_FILE}.tmp" "$EVENT_FILE"
                    fi
                fi
            fi
        fi
    fi

    sleep 1
done

wait "$MONITOR_PID"
WATCHER

chmod 750 "$SCRIPT_PATH"

bash -n "$SCRIPT_PATH"

echo "[7/8] Создаём systemd-сервис..."

cat > "$SERVICE_PATH" <<EOF
[Unit]
Description=Tower folder backup watcher
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
EnvironmentFile=$CONFIG_PATH
ExecStart=$SCRIPT_PATH

Restart=always
RestartSec=5

User=root
UMask=0077
NoNewPrivileges=true
TimeoutStopSec=15

[Install]
WantedBy=multi-user.target
EOF

echo "[8/8] Запускаем сервис..."

systemctl daemon-reload
systemctl enable --now tower-backup-watcher.service
systemctl restart tower-backup-watcher.service

echo
echo "Готово."
echo "Имя архива: ${SERVER_NAME}-backup_<дата>.zip"
echo "Резервный сервер: ${REMOTE_HOST}"
echo "Удалённая папка: ${REMOTE_DIR}"
echo "Локально хранится архивов: ${KEEP_LOCAL}"
echo

systemctl --no-pager --full status tower-backup-watcher.service

Сохрани и сделай исполняемым:

chmod +x setup-tower-backup.sh

2. Как запускать

Пример:

bash setup-node-backup.sh austria-2 192.168.1.50

bash setup-tower-backup.sh austria-2 192.168.1.10

После этого:

  • создастся watcher-скрипт
  • создастся systemd-сервис
  • архивы будут называться так:
austria-2-backup_2026-04-09_14-30-15.zip
  • на резервном сервере они будут складываться сюда:
/root/backup_node/austria-2

3. Что важно

В скрипте забито явное указание имени сервера.
Вот тут оно задаётся напрямую:

bash setup-node-backup.sh austria-2 192.168.1.50

То есть имя не берётся из hostname, не вычисляется автоматически, а передаётся вручную и явно.