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.servicechmod +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.servicechmod +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
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, не вычисляется автоматически, а передаётся вручную и явно.
June 5, 10:04
0 views