ibaranov / db-backup-sender
Почасовое резервное копирование баз MySQL/MariaDB Laravel-проектов с отправкой дампа на удалённый приёмник
Requires
- php: ^8.0
- ext-hash: *
- ext-json: *
- ext-zlib: *
- illuminate/config: ^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/console: ^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/contracts: ^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/database: ^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/http: ^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/log: ^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/mail: ^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/notifications: ^8.0|^9.0|^10.0|^11.0|^12.0
- illuminate/support: ^8.0|^9.0|^10.0|^11.0|^12.0
- symfony/process: ^5.0|^6.0|^7.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.5|^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Composer-пакет для автоматического почасового резервного копирования базы MySQL/MariaDB любого Laravel-проекта с отправкой архива на удалённый приёмник (go-backup-receiver).
Пакет делает всё сам: создаёт дамп mysqldump --single-transaction, сжимает его
в gzip, считает SHA-256, отправляет по HTTPS, удаляет временные файлы, пишет
отдельный лог и присылает письмо при ошибке.
- PHP 8.2+ (проверено на 8.3)
- Laravel 10, 11, 12
- MySQL / MariaDB
- Linux-сервер (на Windows не тестировался)
Содержание
- Как это работает
- Установка
- Настройка .env
- Планировщик
- Команды
- Логи
- Временные файлы
- Уведомления на email
- Требование HTTPS
- Несколько проектов на одном сервере
- Диагностика
- Запуск тестов
- Смена vendor-имени
- Безопасность
Как это работает
php artisan backup:run
│
├─ 1. проверка конфигурации (URL, токен, имя проекта)
├─ 2. проверка наличия mysqldump
├─ 3. mysqldump --single-transaction → dump.sql
├─ 4. gzip → dump.sql.gz
├─ 5. SHA-256 архива
├─ 6. POST multipart на приёмник (токен в заголовке)
└─ 7. удаление временных файлов
Приёмник определяет проект по токену, проверяет контрольную сумму и только
после этого сохраняет файл в подкаталог проекта. Имя файла формирует сервер:
2026-11-04_17-00-00.sql.gz.
При ошибке: запись в отдельный лог, письмо на email, временные файлы
сохраняются для диагностики, код возврата 1. Следующая попытка — по
расписанию через час.
Установка
1. Подключить пакет
cd /path/to/project
composer require your-vendor/db-backup-sender
Пакет поддерживает Laravel Package Auto Discovery: сервис-провайдер
YourVendor\DbBackup\BackupServiceProvider регистрируется автоматически,
команды backup:run и backup:check появляются сразу.
Если авто-обнаружение отключено (composer.json проекта содержит
"extra": {"laravel": {"dont-discover": [...]}}), добавьте провайдер вручную в
config/app.php:
'providers' => [ // ... YourVendor\DbBackup\BackupServiceProvider::class, ],
2. Опубликовать конфигурацию
php artisan vendor:publish --tag=db-backup-config
Появится файл config/db-backup.php со всеми параметрами и комментариями.
Он не обязателен: пакет работает и на значениях по умолчанию из .env.
Публикация нужна, если вы хотите менять параметры в коде проекта.
3. Прописать параметры в .env
BACKUP_ENABLED=true BACKUP_PROJECT_NAME=my-project BACKUP_REMOTE_URL=https://backup.example.com/api/upload BACKUP_TOKEN=вставьте_токен_из_установщика_приёмника BACKUP_EMAIL_ENABLED=true BACKUP_EMAIL=admin@example.com
BACKUP_PROJECT_NAME и BACKUP_TOKEN выдаёт установщик приёмника
(deploy/install.sh): на каждый проект — свой токен, имя проекта совпадает с
именем папки на сервере резервных копий.
4. Проверить готовность
php artisan backup:check
Команда проверит: включён ли пакет, корректно ли имя проекта, валидны ли URL и
токен, отвечает ли приёмник, найден ли mysqldump, доступен ли каталог
временных файлов, работает ли подключение к БД, есть ли таблицы не на InnoDB.
5. Сделать первую копию вручную
php artisan backup:run
Ожидаемый вывод:
Проект: my-project
mysqldump: /usr/bin/mysqldump
Версия клиента: mysqldump Ver 8.0.36 ...
Дамп создан: 12.4 МБ
Архив gzip: 1.8 МБ (сжатие 15% от исходного)
SHA-256: 9f2c...
Резервная копия отправлена успешно
Файл на приёмнике: my-project/2026-11-04_17-00-00.sql.gz
Попытки отправки: 1
Настройка .env
Обязательные параметры
| Переменная | По умолчанию | Описание |
|---|---|---|
BACKUP_PROJECT_NAME |
APP_NAME |
Имя проекта на приёмнике. Только латиница, цифры, ., -, _; до 64 символов |
BACKUP_REMOTE_URL |
— | Полный адрес https://домен/api/upload |
BACKUP_TOKEN |
— | Секретный токен проекта (64 hex-символа) |
Основные
| Переменная | По умолчанию | Описание |
|---|---|---|
BACKUP_ENABLED |
true |
Полное включение/выключение. false — команда завершается успешно и ничего не делает |
BACKUP_HEADER_NAME |
X-Backup-Token |
Имя заголовка с токеном. Должно совпадать с auth_header в конфигурации приёмника. Не используйте _ в имени: Nginx такие заголовки отбрасывает |
BACKUP_TIMEOUT |
180 |
Таймаут HTTP-запроса, секунды |
BACKUP_VERIFY_SSL |
true |
Проверять TLS-сертификат приёмника |
BACKUP_RETRIES |
3 |
Всего попыток отправки (1 — без повторов) |
BACKUP_RETRY_DELAY |
5 |
Пауза между попытками, секунды |
Дамп
| Переменная | По умолчанию | Описание |
|---|---|---|
BACKUP_MYSQLDUMP_PATH |
поиск в PATH |
Полный путь к mysqldump |
BACKUP_DUMP_TIMEOUT |
0 |
Ограничение времени дампа, секунды (0 — без ограничения) |
BACKUP_DB_CONNECTION |
default |
Имя подключения из config/database.php |
BACKUP_CHECK_ENGINES |
true |
Предупреждать о таблицах не на InnoDB |
BACKUP_GZIP_LEVEL |
6 |
Уровень сжатия 1–9 |
Дополнительные аргументы mysqldump задаются только в config/db-backup.php:
'mysqldump' => [ 'extra_arguments' => [ '--set-gtid-purged=OFF', // MySQL 8 с GTID '--column-statistics=0', // клиент MySQL 8 против сервера 5.7 '--skip-routines', // нет прав на процедуры ], ],
Файлы, почта, логи, расписание
| Переменная | По умолчанию | Описание |
|---|---|---|
BACKUP_TEMP_PATH |
storage/app/backups |
Каталог временных дампов (относительно корня проекта) |
BACKUP_EMAIL_ENABLED |
true |
Отправлять письмо при ошибке |
BACKUP_EMAIL |
— | Адрес получателя |
BACKUP_EMAIL_FROM |
config('mail.from') |
Адрес отправителя |
BACKUP_SCHEDULE_ENABLED |
true |
Регистрировать задачу в планировщике |
BACKUP_SCHEDULE_CRON |
0 * * * * |
Расписание (по умолчанию каждый час) |
BACKUP_SCHEDULE_LOCK_MINUTES |
90 |
Сколько держится блокировка от параллельных запусков |
BACKUP_LOG_CHANNEL |
db-backup |
Имя канала логирования |
BACKUP_LOG_PATH |
storage/logs/db-backup.log |
Путь к файлу лога |
BACKUP_LOG_DAYS |
14 |
Сколько дней хранить лог (драйвер daily) |
BACKUP_LOG_LEVEL |
debug |
Уровень логирования |
Служебные варианты (использовать осознанно):
| Переменная | По умолчанию | Описание |
|---|---|---|
BACKUP_ALLOW_INSECURE_HTTP |
false |
Разрешить http:// вместо https://. Только для локальных проверок |
Планировщик
Пакет сам регистрирует задачу:
Schedule::command('backup:run') ->withoutOverlapping(90) // защита от наложения тяжёлых дампов ->cron('0 * * * *'); // каждый час
От Laravel-проекта нужен только стандартный cron планировщика:
* * * * * cd /path/to/project && php artisan schedule:run >> /dev/null 2>&1
Проверка, что задача зарегистрирована:
php artisan schedule:list
Отключить автоматическую регистрацию (например, если вы запускаете
backup:run из собственного cron): BACKUP_SCHEDULE_ENABLED=false.
Важно про InnoDB. --single-transaction даёт согласованный снимок только
для таблиц InnoDB. Для MyISAM (и других не-MySQL движков без транзакций) снимок
может быть несогласованным. Команда backup:check и каждый запуск
backup:run предупреждают об этом в логе и выводе. Если в базе есть MyISAM —
переведите таблицы на InnoDB или делайте эти таблицы отдельным дампом с
блокировкой.
Важно про время выполнения. Задача запускается синхронно: пока дамп
создаётся и отправляется, schedule:run ждёт. Для дампа до 100 МБ это обычно
секунды-десятки секунд. Если в проекте много тяжёлых задач, вынесите резервное
копирование в отдельный cron-вызов php artisan backup:run.
Команды
php artisan backup:run
Создаёт и отправляет резервную копию.
| Опция | Назначение |
|---|---|
--force |
Выполнить даже при BACKUP_ENABLED=false |
--keep-temp |
Не удалять временные файлы после успешной отправки (для отладки) |
--no-notify |
Не отправлять письмо при ошибке |
Коды возврата: 0 — успех или отключённый пакет, 1 — ошибка.
php artisan backup:check
Диагностика без создания дампа. Возвращает 0, если критичных проблем нет, и
1, если что-то настроено неверно.
| Опция | Назначение |
|---|---|
--skip-remote |
Не проверять доступность приёмника (полезно на закрытых стендах) |
Пример вывода:
Диагностика резервного копирования (db-backup-sender)
────────────────────────────────────────────────────────────────────────
[ OK ] Резервное копирование включено да (BACKUP_ENABLED=true)
[ OK ] Имя проекта my-project
[ OK ] Адрес приёмника https://backup.example.com/api/upload
[ OK ] Токен проекта задан, длина 64 символов
[ OK ] Доступность приёмника https://backup.example.com/health — отвечает
[ OK ] Утилита mysqldump /usr/bin/mysqldump — mysqldump Ver 8.0.36
[ OK ] Каталог временных файлов /var/www/project/storage/app/backups
[ OK ] Подключение к БД mysql, база "project", версия 8.0.36
[ OK ] Движки таблиц все таблицы на InnoDB — снимок согласован
[ OK ] Уведомления на email admin@example.com
────────────────────────────────────────────────────────────────────────
Все проверки пройдены.
Логи
Пакет пишет события в отдельный канал db-backup, чтобы они не терялись в
общем логе приложения. По умолчанию — storage/logs/db-backup-YYYY-MM-DD.log
(драйвер daily), 14 дней хранения.
Что логируется:
- старт и параметры запуска (имя проекта, PID, версия PHP);
- путь к
mysqldumpи версия клиента; - размер дампа, размер архива, SHA-256;
- каждая попытка отправки и ответ приёмника;
- предупреждения о таблицах не на InnoDB;
- ошибка с этапом, причиной и путём к сохранённым временным файлам.
Чего в логе нет никогда: пароля БД, токена проекта, содержимого дампа.
Если нужно писать в свой канал, объявите его в config/logging.php и укажите
BACKUP_LOG_CHANNEL=<имя канала> — пакет будет использовать его.
Временные файлы
На каждый запуск создаётся каталог:
storage/app/backups/<project>/<20261104-170000-1a2b3c4d>/
dump.sql # исходный дамп
dump.sql.gz # архив для отправки
- после успешной отправки каталог удаляется;
- при ошибке файлы остаются — дамп нельзя удалять до подтверждённой отправки, иначе разбираться в причине будет нечем (в письме и логе путь указывается). Удалите каталог вручную, когда проблема разобрана;
--keep-tempоставляет файлы и после успеха (для отладки).
Чтобы каталог не рос бесконечно, раз в неделю удаляйте старые:
find storage/app/backups -mindepth 2 -maxdepth 2 -type d -mtime +14 -exec rm -rf {} +
Уведомления на email
Письмо отправляется при любой ошибке резервного копирования, в том числе когда приёмник недоступен:
Тема: Ошибка резервного копирования: my-project
**Проект:** my-project
**Дата:** 04.11.2026 17:00
**Этап:** Отправка на удалённый сервер
**Причина:** приёмник вернул HTTP 401 (invalid_token): неверный токен проекта — проверьте BACKUP_TOKEN
**Попытки:** 3 из 3
**Следующее действие:** повтор при следующем запуске по расписанию.
Используется стандартный почтовый транспорт Laravel, поэтому в .env проекта
должен быть настроен MAIL_MAILER, MAIL_HOST и остальные MAIL_*.
Проверить отправку почты можно штатными средствами Laravel (php artisan tinker).
Требование HTTPS
Передача резервных копий по открытому HTTP запрещена по умолчанию:
backup:run завершится ошибкой ещё до создания дампа.
- рабочая схема: HTTPS с сертификатом Let's Encrypt на поддомене приёмника;
- для локальной отладки можно временно включить
BACKUP_ALLOW_INSECURE_HTTP=true(в лог попадёт предупреждение).
Проверка TLS-сертификата отключается BACKUP_VERIFY_SSL=false, но это снижает
защиту от подмены сервера: используйте только на тестовом стенде.
Несколько проектов на одном сервере
Один приёмник обслуживает любое число Laravel-проектов. На приёмнике у каждого проекта свой токен и своя папка:
"projects": { "project-one": {"token": "…"}, "project-two": {"token": "…"} }
В каждом Laravel-проекте — свой BACKUP_TOKEN и BACKUP_PROJECT_NAME.
Установка пакета в проекты независима: ошибка одного проекта не влияет на
другие (у каждого свой процесс PHP и своя папка на приёмнике).
Добавление нового проекта описано в документации системы
(documentation/INSTALL-RU.md, раздел «Подключение нового Laravel-проекта»).
Диагностика
| Симптом | Вероятная причина | Что делать |
|---|---|---|
утилита mysqldump не найдена в PATH |
не установлен mysql-client | apt install default-mysql-client (MariaDB: mariadb-client), либо указать BACKUP_MYSQLDUMP_PATH |
mysqldump завершился с ошибкой: Access denied |
неверные логин/пароль в config/database.php |
проверить доступы; backup:check покажет версию сервера, если подключение работает |
не задан адрес приёмника |
пустой BACKUP_REMOTE_URL |
прописать URL из документации приёмника |
адрес приёмника должен начинаться с https:// |
указан http:// |
использовать HTTPS или (только локально) BACKUP_ALLOW_INSECURE_HTTP=true |
приёмник вернул HTTP 401 (invalid_token) |
чужой/устаревший токен | взять токен проекта из config.json приёмника |
приёмник вернул HTTP 401 (project_mismatch) |
имя проекта не соответствует токену | BACKUP_PROJECT_NAME должен совпадать с именем проекта на приёмнике |
приёмник вернул HTTP 413 |
файл больше лимита | увеличить max_upload_size в приёмнике и client_max_body_size в Nginx |
приёмник вернул HTTP 422 (checksum_mismatch) |
файл повреждён при передаче | повторить запуск; проверить прокси/антивирус на пути |
приёмник вернул HTTP 422 (bad_format) |
отправлен не gzip | проверить BACKUP_GZIP_LEVEL, версию пакета |
приёмник вернул HTTP 502/504 |
Go-служба не запущена или Nginx не может до неё достучаться | systemctl status backup-receiver, journalctl -u backup-receiver -n 50 |
сетевая ошибка: … Connection refused |
неверный порт/домен, служба остановлена | проверить curl https://backup.example.com/health |
| письмо не приходит | не настроен MAIL_* или стоит BACKUP_EMAIL_ENABLED=false |
проверить настройки почты Laravel и лог db-backup |
| задача не запускается каждый час | не добавлен cron schedule:run |
добавить cron; проверить php artisan schedule:list |
| задача «пропускается» | не истекла блокировка withoutOverlapping |
блокировка живёт до 90 минут; при зависшем процессе удалить ключ из кеша или подождать |
Полный список проблем и решений: documentation/TROUBLESHOOTING.md.
Запуск тестов
composer install vendor/bin/phpunit # все тесты vendor/bin/phpunit --exclude-group integration # без внешних зависимостей
Что покрыто:
- создание дампа (аргументы, безопасная передача пароля, ошибки, пустой дамп);
- gzip-сжатие и распаковка;
- SHA-256;
- отправка: заголовок, multipart-поля, повторы, отказ при 4xx, таймауты;
- отключённый режим,
--force,--keep-temp,--no-notify; - удаление временных файлов и сохранение их при ошибке;
- письмо об ошибке (содержимое и отсутствие секретов);
- регистрация в планировщике и публикация конфигурации;
- интеграция с настоящим Go-приёмником (тест
EndToEndTest, группаintegration); - тест с настоящим
mysqldump(группаintegration, включается переменнойBACKUP_TEST_MYSQL_URL='mysql://user:pass@127.0.0.1:3306/db').
Интеграционный тест находит бинарник приёмника по пути
../go-backup-receiver/.local/backup-receiver-darwin-* или в
bin/backup-receiver-linux-*; путь можно задать явно:
BACKUP_RECEIVER_BIN=/path/to/backup-receiver vendor/bin/phpunit --group integration
Смена vendor-имени
Пакет поставляется с условным пространством имён YourVendor\DbBackup.
Чтобы выпустить его под своим именем, замените your-vendor и YourVendor
в composer.json, src/, tests/:
# Linux grep -rl 'YourVendor' composer.json src tests \ | xargs sed -i 's/YourVendor/Acme/g' sed -i 's#your-vendor/db-backup-sender#acme/db-backup-sender#' composer.json
# macOS grep -rl 'YourVendor' composer.json src tests \ | xargs sed -i '' 's/YourVendor/Acme/g' sed -i '' 's#your-vendor/db-backup-sender#acme/db-backup-sender#' composer.json
После этого выполните composer dump-autoload и прогоните тесты.
Безопасность
- Пароль MySQL передаётся
mysqldumpтолько через временный файл опций с правами0600и удаляется вfinally. В аргументах процесса и в переменных окружения пароля нет. - Токен передаётся только в HTTP-заголовке, никогда не пишется в лог и не попадает в текст исключений (вырезается даже из ответа сервера).
- Передача разрешена только по HTTPS; сертификат проверяется.
- Временные файлы создаются с правами
0750(каталог) и внутриstorage/app. - Контрольная сумма SHA-256 защищает от порчи файла при передаче.
Что не реализовано (по требованиям этапа): шифрование дампа на стороне клиента, HMAC-подпись запроса. Данные защищены транспортом (TLS) и токеном.
Лицензия
MIT, см. LICENSE.