Что должен уметь скрипт бэкапа в 2026 году
Рабочий пайплайн резервного копирования собирается из шести блоков: создание копий (rsync, tar, mysqldump), ротация, контроль свободного места на целевом хранилище, проверка целостности, тестовое восстановление и уведомление о сбоях. Уберите любой блок, и на руках останется набор файлов неизвестного качества.
Молча сломавшийся бэкап - самый неприятный сценарий: все уверены, что копии есть, а их нет уже месяц. Скрипт завершается с кодом 0, cron пишет обычный запуск, дамп при этом пустой или вдвое меньше ожидаемого. Вскрывается это в момент аварии, когда восстанавливать уже нечего: только ручная проверка логов и дат файлов в каталоге копий показывает пропуск. Такую картину подробно разбирают на примере автоматических копий VPS, и она повторяется почти в каждой инфраструктуре, где скрипт писали один раз и больше к нему не возвращались.
Дальше - готовый /opt/backup.sh по блокам, cron-задача на 4:30, файл секретов, exclude-лист, скрипт проверки целостности и restore-тест на staging. Все фрагменты копируются как есть, подставлять нужно только свои пути, имена баз и UUID мониторинга.
Шесть требований, без которых бэкап не считается рабочим
- set -euo pipefail в первой строке. Без этого сломавшийся mysqldump создаст пустой дамп, а скрипт дойдёт до конца и отрапортует об успехе.
- Пароли в отдельном файле. /opt/backup.env с правами 600 и владельцем backup, подключение через set -a && source /opt/backup.env && set +a.
- Ротация. find "$BACKUP_DIR" -type f -mtime +$KEEP_DAYS -delete, минимум неделя ежедневной глубины.
- Копия вне площадки. S3-совместимое хранилище, второй сервер или NAS в другом месте: локальные копии не помогут при полной потере сервера или доступе постороннего.
- Внешний healthcheck. Dead man's switch в сервисе мониторинга: нет пинга - есть письмо.
- Restore-тест по расписанию. Проверка целостности подтверждает, что архив не битый. Только восстановление подтверждает, что из него поднимется сервис.
Каркас скрипта с trap, IFS и обработкой кодов возврата разбираем отдельно: структура bash-скрипта, set -euo pipefail и чек-лист для ревью перед выкаткой в прод.
Готовый скрипт бэкапа: rsync, tar и mysqldump
Скрипт лежит в /opt/backup.sh, владелец backup, права 700. Разбираем по блокам: переменные, дамп базы, архивы конфигов, инкрементальная синхронизация rsync, ротация, healthcheck. Каждый блок работает самостоятельно, поэтому порядок строк важен: сначала данные, потом удаление старого, в самом конце сигнал мониторингу.
Блок 1. Переменные и подготовка каталога.
set -euo pipefail
BACKUP_DIR=/backup
DATE=$(date +%F_%H-%M)
KEEP_DAYS=7
mkdir -p "$BACKUP_DIR"
Блок 2. Дамп базы данных. Флаг --single-transaction даёт консистентный снимок InnoDB без блокировки таблиц, а --routines и --triggers забирают процедуры и триггеры, без которых восстановление даст половину схемы.
mysqldump --single-transaction --routines --triggers -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" | gzip > "$BACKUP_DIR/db-$DATE.sql.gz"
Блок 3. Конфиги, службы и сертификаты. Без этого архива вы восстановите данные, но не поднимете веб-сервер и не соберёте обратно TLS.
tar -czf "$BACKUP_DIR/config-$DATE.tar.gz" /etc/nginx /etc/systemd/system /etc/letsencrypt 2>/dev/null
Блок 4. Инкрементальная синхронизация каталогов приложений. Ключи -aAXH сохраняют права, ACL, расширенные атрибуты и жёсткие ссылки. Флаг --delete стоит применять осознанно: он удалит в копии то, что пропало в источнике, поэтому для истории держите рядом каталог версий через --backup-dir или второй приёмник без --delete.
rsync -aAXH --delete --exclude-from=/opt/backup.exclude /var/www/ /backup/www/
Блок 5. Ротация. Удаление копий старше KEEP_DAYS идёт после успешной записи, иначе одна ошибка оставит вас без истории.
find "$BACKUP_DIR" -type f -mtime +$KEEP_DAYS -delete
Блок 6. Healthcheck. Строка выполняется в самом конце, поэтому сигнал уходит только при полностью успешном прогоне.
curl -fsS -m 10 "$HC_URL" > /dev/null
Почему set -e и pipefail обязательны
Представьте: mysqldump упал из-за неверного пароля или недоступного сокета. Без set -e скрипт шагает дальше, доходит до отправки healthcheck, вы получаете зелёный статус, а рядом с рабочими копиями лежит архив на 20 байт. С set -euo pipefail выполнение обрывается на первой ошибке, до строки с curl дело не доходит, мониторинг фиксирует пропуск сигнала и присылает письмо. Это и есть разница между «бэкап есть» и «мы думаем, что бэкап есть».
pipefail нужен именно для пайпов. В строке mysqldump ... | gzip код возврата по умолчанию берётся от последней команды, а gzip успешно создаст корректный архив из нуля байт. С pipefail статус всего пайпа равен статусу первой упавшей команды.
Пароли и секреты: /opt/backup.env
Секреты держим отдельно от логики. Файл /opt/backup.env, права 600, владелец - сервисный пользователь backup.
DB_USER=backup_ro
DB_PASS=СМЕНИТЕ_НА_СВОЙ_ПАРОЛЬ
DB_NAME=app
HC_URL=https://hc-ping.com/ВАШ-UUID
Подключение в скрипте: set -a && source /opt/backup.env && set +a. Опция -a помечает переменные на экспорт, поэтому их видят дочерние процессы mysqldump, tar и curl. В cron-задаче тот же файл подключается отдельно, точка заменяет команду source и работает в базовой оболочке crontab.
Пароль, вписанный прямо в скрипт, утекает при любом чтении файла: бэкап каталога /opt, вывод в лог, история команд, копия репозитория. Пользователь backup получает права чтения каталогов приложения и записи в /backup, а дамп базы выполняется от учётки только на чтение, чтобы скомпрометированный скрипт не мог удалить данные.
Что бэкапить и что исключать
Обязательный минимум: /etc/nginx, /etc/systemd/system, /etc/letsencrypt, каталоги приложений /var/www, дампы баз, .env-файлы приложений в шифрованном архиве. Отдельно забирайте файлы юнитов и конфиги systemd, потому что после переустановки сервера приложение без них просто не стартует.
Исключайте всё, что скачивается заново: образы и слои Docker, node_modules, vendor, кэши приложений, /var/log, /tmp, /proc, /sys, /dev. Это раздувает архивы в разы без всякой пользы и замедляет выгрузку за пределы площадки.
Файл /opt/backup.exclude:
node_modules/
vendor/
cache/
*.log
tmp/
proc/
sys/
dev/
docker/overlay2/
Принцип 3-2-1 остаётся базовой схемой: три копии, два разных носителя, одна копия вне основной площадки. Скрипт закрывает первый пункт, ротация и выгрузка - остальные два.
Cron, логи и уведомления о сбоях
Расписание выглядит так:
30 4 * * * . /opt/backup.env && /opt/backup.sh >> /var/log/backup.log 2>&1
Разбираем строку. Точка перед путём - встроенный source, который подтягивает переменные до старта скрипта. Двойная стрелка >> дописывает вывод в конец файла, а 2>&1 направляет поток ошибок туда же. Без перенаправления сообщения об ошибках уйдут в почту cron или исчезнут вместе с сессией, и вы останетесь без следов разбора.
Лог /var/log/backup.log ротируется через logrotate: weekly, rotate 8, compress, missingok, notifempty. Восемь недель истории достаточно, чтобы увидеть, когда именно бэкап начал деградировать по объёму.
Внешний healthcheck закрывает главный сценарий: dead man's switch в сервисе мониторинга присылает письмо, если скрипт не отчитался вовремя. Бесплатные сервисы вроде healthchecks.io решают это одним curl-запросом в конце скрипта: успех - сигнал ушёл, сбой - тишина - алерт. Альтернативы для тех, кто не отдаёт данные наружу: Uptime Kuma с push-монитором, self-hosted решения или Telegram-бот с отправкой сообщения через sendMessage.
Даже с алертами раз в неделю смотрите лог и даты файлов: ls -lh /backup | tail. Если последний db-*.sql.gz старше суток, проблема уже есть. Если сбой случился и копия помечена Success, но восстановление не проверялось, поможет диагностика ошибок резервного копирования и проверка восстановимости.
Dead man's switch: почему healthcheck надёжнее проверки exit code
Проверка кода возврата работает только тогда, когда скрипт запустился. Сервер лежит, cron не сработал, диск переполнен настолько, что не пишется ни лог, ни сам скрипт: exit code просто не существует, и проверять нечего. Dead man's switch ловит отсутствие сигнала, а не его значение.
Практические настройки при ежедневном бэкапе в 4:30: период ожидания 26 часов, grace-период 1 час. Такой запас поглощает дрейф длительности ночного прогона, короткий перезапуск сервера и переход на летнее время.
Ротация бэкапов и контроль свободного места
Простая ротация выполняется одной строкой в конце скрипта: find "$BACKUP_DIR" -type f -mtime +$KEEP_DAYS -delete. При KEEP_DAYS=7 каталог держит неделю ежедневных копий и не растёт бесконечно.
Для длинной истории нужна GFS-схема: 7 daily, 4 weekly, 12 monthly. Копии раскладываются по каталогам /backup/daily, /backup/weekly, /backup/monthly, и каждый уровень чистится своим порогом: find /backup/daily -type f -mtime +7 -delete, для weekly -mtime +28, для monthly -mtime +365. Ежедневная копия перед переносом в weekly переименовывается, чтобы не потерять консистентность пары «дамп базы плюс архив файлов».
Рост контролируется через du -sh /backup раз в неделю: тренд важнее абсолютной цифры. Резкий скачок означает либо сломанную ротацию, либо данные, которые вы не заметили. Если бэкап перестал помещаться, сначала разберитесь с причиной, потом покупайте диск.
Проверка свободного места до запуска бэкапа
Проверка ставится в начало скрипта, до первой записи.
FREE=$(df -Pk "$BACKUP_DIR" | awk 'NR==2 {print $4}')
if [ "$FREE" -lt 5242880 ]; then echo "Not enough space in $BACKUP_DIR" >&2; exit 1; fi
Порог 5242880 КБ равен 5 ГБ. Проверяйте и абсолютный объём, и долю: если ночная копия занимает 40% диска, ставьте порог в 10% от общего размера. Выход с кодом 1 вместо продолжения выбран осознанно: одна пропущенная ночь дешевле, чем забитый до нуля диск, на котором лежат логи, сокеты и данные приложения.
Healthcheck в этой ветке не отправляется, поэтому алерт придёт сам. Дополнительное уведомление в Telegram удобно вешать в ту же ветку отдельным вызовом бота.
Проверка целостности резервной копии
Целостность проверяют до аварии, а не во время неё. Уровней три: бинарная целостность архива, валидность дампа базы и контрольные суммы файлов. Проверка выносится в отдельный скрипт /opt/backup-verify.sh и запускается cron через час после основного: за это время запись завершена, архивы закрыты, ротация отработала. В конце скрипта - healthcheck, который уходит только при успехе всех проверок.
Контрольные суммы снимаются сразу после создания копий: sha256sum "$BACKUP_DIR"/*-"$DATE".* > "$BACKUP_DIR/checksums-$DATE.txt", при восстановлении - sha256sum -c checksums-$DATE.txt. Это ловит битую передачу на внешнее хранилище, которую не увидит проверка архива на исходном сервере.
Проверка tar-архивов и gzip-дампов
tar -tzf config-$DATE.tar.gz > /dev/null читает архив без распаковки: код возврата 0 при целостности, ненулевой при повреждении. Для сжатых дампов та же логика у gunzip -t db-$DATE.sql.gz.
Типовое сообщение о повреждении: gzip: db-2026-09-25_04-30.sql.gz: unexpected end of file. Причина почти всегда одна - оборванная запись из-за кончившегося места или разрыва сети при выгрузке.
Архив на 10 ГБ проверяется минуты, поэтому не встраивайте эту операцию в основной скрипт: ночной бэкап начнёт пересекаться сам с собой, а окно обслуживания растянется на весь день.
Проверка дампа БД: mysqlcheck и тестовая схема
Быстрая отсечка: дамп меньше 1 КБ почти наверняка пустой. Дальше - восстановление в отдельную схему. Распаковываете gunzip -c db-$DATE.sql.gz и заливаете в test_restore: mysql -u"$DB_USER" -p"$DB_PASS" test_restore < dump.sql, затем сверяете SELECT COUNT(*) FROM test_restore.users с продовой таблицей.
mysqlcheck -u"$DB_USER" -p"$DB_PASS" test_restore проверяет таблицы на повреждения. Расхождение в счётчиках строк больше суточного прироста (или больше 1%) означает, что дамп снят не полностью. Флаг --single-transaction полезен именно здесь: он даёт консистентный снимок InnoDB без блокировок, поэтому цифры в дампе сходятся с продом.
Тестовое восстановление из бэкапа: пошаговый сценарий
Целостный архив ещё не доказывает, что сервис поднимется. Восстановление проверяется на staging или в изолированном каталоге /restore на отдельном диске, чтобы не задеть прод.
- Поднять staging-сервер или каталог /restore с достаточным объёмом.
- Распаковать конфиги: tar -xzf config-$DATE.tar.gz -C /restore.
- Развернуть базу в test_restore и прогнать mysqlcheck.
- Синхронизировать файлы приложения: rsync -aAXH --dry-run /backup/www/ /restore/www/ - сначала в режиме dry-run, чтобы увидеть объём изменений и не снести лишнего.
- Запустить приложение на staging и проверить curl -f http://localhost:8080/healthz, вход в админку и одну-две ключевые операции.
- Сверить контрольные суммы и зафиксировать фактическое время восстановления: оно должно укладываться в RTO, а частота бэкапов - в RPO.
Скрипт /opt/restore-test.sh запускается по расписанию и в конце отправляет healthcheck, чтобы процедура не зависела от человеческой памяти. Схемы автотестов восстановления и связки rsync с BorgBackup и Rclone разобраны в материале резервное копирование сервера 2026: rsync, Borg, Rclone и автотесты восстановления.
Чек-лист успешного восстановления
- все ожидаемые файлы на месте: diff -r /etc/nginx /restore/etc/nginx без расхождений;
- база восстановлена, количество строк в ключевых таблицах совпадает с продом в пределах суточного окна;
- приложение стартует и отвечает на /healthz;
- контрольные суммы совпадают: sha256sum -c checksums-$DATE.txt;
- время восстановления замерено и занесено в RTO.
Не выполнен хотя бы один пункт - бэкап не считается рабочим. Заводите инцидент и разбирайтесь до следующего запуска, а не после следующей аварии.
Как часто тестировать восстановление
| Категория сервисов | Частота restore-теста | Что проверяем |
|---|---|---|
| Критичные: платежи, авторизация | раз в месяц | полный цикл: база, конфиги, старт приложения |
| Внутренние сервисы | раз в квартал | база и конфиги, выборочная сверка файлов |
| Архивные данные | раз в полгода | целостность архивов и выборочное извлечение |
Ручной restore-тест раз в год - это лотерея: между проверками успевает смениться версия СУБД, структура таблиц и схема каталогов. Автоматизация через cron и healthcheck убирает зависимость от того, вспомнит ли кто-то о процедуре в пятницу вечером.
Снапшоты VPS vs скриптовые бэкапы: что выбрать
Снапшот - это всё или ничего: достать один случайно удалённый файл или вчерашнюю версию одной таблицы не получится, и хранится он на той же инфраструктуре, что и сам сервер. Разбор бэкапов VPS прямо указывает на это ограничение: снапшот не заменяет гранулярную копию и не защищает от потери площадки целиком.
Сильная сторона снапшота - скорость. Он создаётся за секунды и выручает перед рискованными действиями: обновление системы, миграция базы, смена конфигурации веб-сервера. Слабая сторона - отсутствие гранулярности, версионирования отдельных файлов и копии вне площадки.
Скриптовый бэкап даёт гранулярность, внешнюю копию, историю версий и проверку целостности. Выгрузку на внешнее хранилище удобно делать через rclone, а проверку контрольных сумм и повторные попытки при обрывах сети разбираем в статье про автоматизацию загрузки и выгрузки файлов, CLI и интеграцию с CI/CD.
| Критерий | Снапшот VPS | Скриптовый бэкап |
|---|---|---|
| Скорость полного восстановления | минуты | десятки минут |
| Гранулярность | нет | файл, таблица, каталог |
| Копия вне площадки | нет | да: S3, NAS, второй сервер |
| Защита от потери площадки | нет | да |
| Стоимость хранения | зависит от тарифа провайдера | зависит от объёма и класса хранилища |
| Сценарий применения | перед рискованными операциями | ежедневная регулярная копия |
Рабочая связка для продакшена: снапшот перед рискованной операцией плюс ежедневный скриптовый бэкап с копией вне площадки. Снапшот страхует от «сломал конфиг», скрипт - от потери сервера целиком.
Безопасность бэкапов: пароли, шифрование, доступ
Секреты живут только в /opt/backup.env с правами 600 и владельцем backup. Скрипт получает права 700 и запускается от отдельного пользователя backup, а не от root: компрометация скрипта не должна давать полный доступ к системе.
Архивы с чувствительными данными шифруйте до выгрузки: gpg --symmetric --cipher-algo AES256 archive.tar.gz или age -p archive.tar.gz. Ключ храните отдельно от копий - на другом сервере или в менеджере секретов. Ключ, лежащий рядом с архивом, обнуляет шифрование целиком. Если в бэкап попадает хранилище паролей, схема усложняется: смотрите практическое руководство по бэкапу хранилищ паролей KeePass и Vaultwarden с шифрованием через age и GPG.
На стороне внешнего хранилища: отдельный ключ доступа с правом записи в один бакет, включённое версионирование, Object Lock или режим immutable, если провайдер его поддерживает. Ключ с правом удаления объектов не должен лежать на том же сервере, где работает скрипт.
Финальная проверка на утечку: убедитесь, что /opt/backup.env не попал в tar-архив конфигов. Одна строка архивации с корнем /etc или /opt, и пароль от базы лежит открытым текстом на внешнем хранилище.
Чек-лист: готов ли ваш бэкап к продакшену
- set -euo pipefail в первой строке скрипта (раздел про требования к скрипту).
- Пароли в /opt/backup.env с правами 600 и владельцем backup (раздел про секреты).
- Cron-задача с логированием через >> и 2>&1 (раздел про cron и логи).
- Ротация через find -mtime +$KEEP_DAYS -delete (раздел про ротацию).
- Проверка свободного места до старта бэкапа с выходом по exit 1 (раздел про контроль места).
- Копия вне площадки: S3-совместимое хранилище, NAS или второй сервер (разделы про требования и снапшоты).
- Внешний healthcheck с периодом 26 часов и grace-периодом 1 час (раздел про dead man's switch).
- Проверка целостности tar и gzip отдельным скриптом (раздел про проверку целостности).
- Проверка дампа базы: размер, mysqlcheck, сверка счётчиков строк (раздел про дампы).
- Restore-тест по расписанию с заполненным чек-листом восстановления (раздел про тестовое восстановление).
- Шифрование архивов с чувствительными данными, ключ вне сервера (раздел про безопасность).
- Документированная процедура восстановления: кто, что и в каком порядке запускает при аварии.
Прогоните этот список сегодня, а не в день инцидента. Каждый непройденный пункт - это готовый сценарий, в котором бэкап не восстановится.