mysqldump по cron — самый распространённый способ бэкапа MySQL на VPS, и в нём легко ошибиться: пароль в командной строке, неконсистентный дамп MyISAM-таблиц, забытые процедуры и триггеры, бесконечно растущий каталог с копиями. В этой статье — правильные флаги, безопасное хранение пароля, скрипт с ротацией daily/weekly/monthly, загрузка в российское S3 и проверка восстановления.
Флаги, без которых дамп неполный или неконсистентный
Базовая команда для InnoDB-базы:
mysqldump --defaults-extra-file=/etc/mysql/backup.cnf \
--single-transaction --quick \
--routines --triggers --events \
--hex-blob --default-character-set=utf8mb4 \
--no-tablespaces \
app > app.sql
Что делает каждый флаг:
--single-transactionоткрывает транзакцию с консистентным снимком (START TRANSACTION WITH CONSISTENT SNAPSHOT). Для InnoDB это даёт согласованный дамп без блокировки записи. Для MyISAM-таблиц снимок не работает: их нужно блокировать (--lock-all-tablesили--lock-tables), а это останавливает запись на время дампа. Не сочетайте--single-transactionс--lock-tables. И не запускайте DDL (ALTER,DROP,RENAME,TRUNCATE) во время дампа — снимок от них не защищает.--quickчитает строки потоком, а не загружает таблицу в память целиком. Включён по умолчанию через--opt, но явное указание не помешает.--routines --eventsдобавляют хранимые процедуры, функции и события планировщика — по умолчанию их в дампе нет.--triggersвключён по умолчанию, но лучше указать явно.--hex-blobвыгружает бинарные колонки (BLOB,BINARY,VARBINARY) в шестнадцатеричном виде, чтобы байты не испортились при перекодировке.--default-character-set=utf8mb4гарантирует, что эмодзи и четырёхбайтовые символы доедут без искажений, особенно на старых серверах, где по умолчанию стоялutf8(он жеutf8mb3).--no-tablespacesубирает информацию о табличных пространствах. Начиная с MySQL 8.0.21 для её выгрузки нужна привилегияPROCESS; без флага дамп от ограниченного пользователя упадёт сAccess denied ... PROCESS privilege.
--all-databases или по одной базе
--all-databases выгружает всё, включая системную базу mysql с пользователями. Это удобно для полного переезда на ту же версию сервера, но такой дамп сложно загрузить в другую версию и нельзя восстановить одну базу отдельно. На практике удобнее дампить каждую пользовательскую базу в свой файл, а учётные записи сохранять отдельно — через SHOW CREATE USER и SHOW GRANTS.
MariaDB
В MariaDB основной бинарник называется mariadb-dump; имя mysqldump оставлено для совместимости, но в новых сборках этой ссылки может не быть. Флаги из примера выше работают так же. GTID в MariaDB устроены иначе, поэтому --set-gtid-purged там нет — у mariadb-dump для этого свой флаг --gtid.
GTID и позиция бинлога
Если на сервере MySQL включены GTID, mysqldump по умолчанию добавляет в дамп SET @@GLOBAL.GTID_PURGED=.... При загрузке на сервер, где уже есть свои GTID (например, на другой рабочий сервер), это вызовет ошибку. Если дамп не предназначен для создания реплики, добавьте --set-gtid-purged=OFF.
Чтобы потом докатить изменения из бинлогов (восстановление на момент времени), запишите в дамп позицию бинлога: --source-data=2 (MySQL 8.0.26+, пишет CHANGE REPLICATION SOURCE TO ... в виде комментария). Старое имя флага --master-data объявлено устаревшим. Для этого флага пользователю нужна привилегия RELOAD.
Пароль: defaults-extra-file вместо -p
mysqldump -uroot -pSECRET — плохая идея: пароль виден в ps любому пользователю сервера и попадает в историю shell. Положите учётные данные в файл с правами 600:
# /etc/mysql/backup.cnf
[client]
user = backup
password = очень-длинный-пароль
host = 127.0.0.1
sudo chown root:root /etc/mysql/backup.cnf
sudo chmod 600 /etc/mysql/backup.cnf
--defaults-extra-file обязательно указывается первым аргументом, иначе клиент его не прочтёт. Переменная окружения MYSQL_PWD тоже работает, но документация MySQL называет её небезопасной: окружение процесса можно прочитать.
Отдельный пользователь с минимальными правами:
CREATE USER 'backup'@'localhost' IDENTIFIED BY 'очень-длинный-пароль';
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, EVENT, PROCESS, RELOAD ON *.* TO 'backup'@'localhost';
PROCESS нужен, если вы не указываете --no-tablespaces. RELOAD нужен для --source-data, а в свежих версиях MySQL 8.x — и для дампа с --single-transaction на сервере с GTID, если не указан --set-gtid-purged=OFF. Если ваш сценарий без этого обходится, эти две привилегии можно не выдавать.
Скрипт с GFS-ротацией и загрузкой в S3
GFS (grandfather-father-son) — схема, в которой вы храните последние ежедневные копии, несколько еженедельных и несколько ежемесячных. Так у вас есть и вчерашний бэкап, и состояние трёхмесячной давности, а место не растёт бесконечно.
#!/usr/bin/env bash
# /usr/local/bin/mysql-backup.sh
set -euo pipefail
CNF="/etc/mysql/backup.cnf"
ROOT="/var/backups/mysql"
BUCKET="s3://my-mysql-backups"
ENDPOINT="https://storage.yandexcloud.net" # Selectel: https://s3.ru-1.storage.selcloud.ru
STAMP="$(date -u +%Y-%m-%dT%H%M%SZ)"
mkdir -p "$ROOT"/{daily,weekly,monthly}
DATABASES=$(mysql --defaults-extra-file="$CNF" -N -e "SHOW DATABASES" \
| grep -Ev '^(information_schema|performance_schema|sys|mysql)$')
for DB in $DATABASES; do
FILE="$ROOT/daily/${DB}-${STAMP}.sql.zst"
mysqldump --defaults-extra-file="$CNF" \
--single-transaction --quick --routines --triggers --events \
--hex-blob --default-character-set=utf8mb4 --no-tablespaces \
--set-gtid-purged=OFF \
"$DB" | zstd -q -T0 -3 -o "$FILE.part"
zstd -q -t "$FILE.part"
mv "$FILE.part" "$FILE"
# воскресенье — копия в weekly, 1-е число — в monthly
[ "$(date +%u)" = "7" ] && cp "$FILE" "$ROOT/weekly/"
[ "$(date +%d)" = "01" ] && cp "$FILE" "$ROOT/monthly/"
aws s3 cp "$FILE" "$BUCKET/daily/$(basename "$FILE")" \
--endpoint-url "$ENDPOINT" --profile backup --only-show-errors
done
find "$ROOT/daily" -type f -name '*.sql.zst' -mtime +7 -delete
find "$ROOT/weekly" -type f -name '*.sql.zst' -mtime +35 -delete
find "$ROOT/monthly" -type f -name '*.sql.zst' -mtime +365 -delete
Детали:
set -o pipefailобязателен: без него при паденииmysqldumpкод возврата возьмётся отzstd, и вы получите «успешный» пустой архив.- Если на сервере нет GTID,
--set-gtid-purged=OFFничего не меняет. Для MariaDB этот флаг уберите. - Дамп пишется без
--databases, то есть безCREATE DATABASEиUSE. Такой файл можно загрузить в базу с любым именем — это удобно для тестового восстановления. zstdобычно сжимает быстрееgzipпри сопоставимой степени сжатия; если его нет на сервере, замените наgzip -6иgzip -t.- Если базы называются с пробелами или спецсимволами, цикл по
$DATABASESпридётся переписать наwhile read.
В S3 ротацию удобнее отдать правилу жизненного цикла бакета, указав разные сроки для префиксов daily/, weekly/, monthly/ (weekly и monthly загружайте отдельно, по аналогии с daily). Пример JSON-правила и команды aws s3api put-bucket-lifecycle-configuration, а также настройку rclone для Yandex Object Storage и Selectel смотрите в статье про бэкап PostgreSQL в S3 — для MySQL всё то же самое.
Расписание: cron + flock и systemd timer
# /etc/cron.d/mysql-backup
30 2 * * * root flock -n /run/mysql-backup.lock /usr/local/bin/mysql-backup.sh >> /var/log/mysql-backup.log 2>&1
flock -n не даст запустить второй экземпляр, если вчерашний дамп ещё идёт. Вариант с systemd:
# /etc/systemd/system/mysql-backup.service
[Unit]
Description=MySQL logical backup
After=network-online.target mysql.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/mysql-backup.sh
Nice=10
IOSchedulingClass=idle
# /etc/systemd/system/mysql-backup.timer
[Unit]
Description=Nightly MySQL backup
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now mysql-backup.timer
Persistent=true выполнит пропущенный запуск после перезагрузки, а Nice и IOSchedulingClass=idle снизят влияние дампа на рабочую нагрузку.
Windows
На Windows-сервере тот же дамп запускается из PowerShell. Пишите в файл через --result-file, а не через >: Windows PowerShell 5.1 перекодирует перенаправленный вывод в UTF-16.
$mysqldump = 'C:\Program Files\MySQL\MySQL Server 8.4\bin\mysqldump.exe'
$out = "D:\Backups\mysql\app-$((Get-Date).ToString('yyyy-MM-dd')).sql"
& $mysqldump --defaults-extra-file=C:\Scripts\backup.cnf --single-transaction `
--routines --triggers --events --hex-blob --no-tablespaces --result-file=$out app
if ($LASTEXITCODE -ne 0) { throw "mysqldump завершился с кодом $LASTEXITCODE" }
Задание в планировщике создаётся через Register-ScheduledTask, как в примере для PostgreSQL по ссылке выше.
Проверка восстановления
Бэкап, который ни разу не восстанавливали, — это предположение. Самый простой тест — одноразовый контейнер той же версии MySQL:
docker run -d --rm --name restore-test -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql:8.4
# ждём, пока сервер начнёт принимать TCP-подключения (временный сервер инициализации их не принимает)
until docker exec restore-test mysql --protocol=tcp -h127.0.0.1 -uroot -e 'SELECT 1' >/dev/null 2>&1; do sleep 2; done
docker exec restore-test mysql --protocol=tcp -h127.0.0.1 -uroot -e 'CREATE DATABASE app_restore'
zstd -dc /var/backups/mysql/daily/app-2026-09-15T023000Z.sql.zst \
| docker exec -i restore-test mysql --protocol=tcp -h127.0.0.1 -uroot app_restore
docker exec restore-test mysql --protocol=tcp -h127.0.0.1 -uroot -e 'SELECT COUNT(*) FROM app_restore.users'
docker stop restore-test
Обратите внимание на docker exec -i без -t: при передаче дампа через конвейер псевдотерминал не нужен и может испортить поток. Сравнивайте COUNT(*) ключевых таблиц с рабочей базой — TABLE_ROWS из information_schema для InnoDB лишь приблизительная оценка. Полный чек-лист — в статье как проверить, что бэкап рабочий, а команды восстановления для всех СУБД — в материале как восстановить базу из бэкапа.
Когда mysqldump становится тесно
mysqldump однопоточный, а восстановление дампа — это повторное выполнение всех INSERT с перестроением индексов. На больших базах это долго. Варианты:
- mydumper / myloader — логический дамп и загрузка в несколько потоков, каждая таблица в своём файле, консистентный снимок. Логика та же, что у
mysqldump, но быстрее и с выборочным восстановлением. - MySQL Shell (
util.dumpInstance,util.loadDump) — официальная параллельная выгрузка от Oracle. - Percona XtraBackup (для MariaDB —
mariadb-backup) — физический «горячий» бэкап InnoDB без остановки сервера. Восстанавливается копированием файлов, а не выполнением SQL. В связке с бинлогами даёт восстановление на момент времени. Версия XtraBackup должна соответствовать версии сервера.
Переходить стоит, когда восстановление из mysqldump перестаёт укладываться в допустимое время простоя или когда суточные потери между бэкапами становятся неприемлемыми.
Как это сделать в dbsend
dbsend не подключается к MySQL и не запускает mysqldump за вас: дамп делает ваш скрипт, а CLI загружает готовый файл. Сервис принимает plain-дампы MySQL и MariaDB (движок mysql_dump), разбирает схему и считает строки по таблицам. Загружайте несжатый .sql — CLI сжимает его при передаче сам.
npm install --global @dbsend/sdk
export DBSEND_API_KEY='sqv_pk_…'
mysqldump --defaults-extra-file=/etc/mysql/backup.cnf --single-transaction \
--routines --triggers --events --hex-blob --no-tablespaces --result-file=/var/backups/mysql/app.sql app
dbsend backup /var/backups/mysql/app.sql -d "$DBSEND_DATABASE_ID" -e mysql_dump -l "nightly-$(date +%F)"
Каждый дамп становится версией; если он побайтно совпадает с предыдущим, новая версия не создаётся. Между любыми двумя версиями можно посмотреть diff: изменения схемы, таблиц, числа строк и размера — так видно, что ночью из orders пропала половина записей. Если дамп не пришёл по расписанию, dbsend присылает уведомление в Telegram, на почту или в webhook. Хранение — S3 в России с шифрованием AES-256-GCM. Diff и уведомления доступны на платных тарифах, условия — на странице тарифов. Подробнее о возможностях — на странице бэкапов MySQL и в документации CLI. Историю версий и diff можно смотреть и из Claude Code или Cursor через MCP-сервер; сам файл дампа при этом по-прежнему загружает CLI.
Если MySQL работает в контейнере, особенности docker exec и встроенных бэкапов Coolify разобраны в статье о бэкапе базы в Docker и Coolify.
Частые вопросы
Блокирует ли mysqldump таблицы?
С --single-transaction InnoDB-таблицы не блокируются для записи: дамп читает консистентный снимок. MyISAM-таблицы так защитить нельзя — для согласованного дампа их приходится блокировать. Если в базе остались MyISAM-таблицы, переведите их на InnoDB (ALTER TABLE ... ENGINE=InnoDB).
Почему mysqldump пишет «Access denied; you need the PROCESS privilege»?
С MySQL 8.0.21 выгрузка информации о табличных пространствах требует привилегии PROCESS. Добавьте флаг --no-tablespaces или выдайте пользователю бэкапа PROCESS.
Как хранить пароль для mysqldump в cron?
В файле [client] с правами 600 и параметром --defaults-extra-file, указанным первым аргументом. Не передавайте пароль через -p в командной строке: его видно в списке процессов.
Сколько копий хранить?
Зависит от того, как поздно вы можете обнаружить проблему. Схема GFS — 7 ежедневных, 4–5 еженедельных и 12 ежемесячных — покрывает и вчерашнюю ошибку, и порчу данных, замеченную через месяц. Минимум одна копия должна лежать вне сервера с базой.
Подходит ли mysqldump для MariaDB?
Да, но в новых версиях MariaDB используйте mariadb-dump. Флаги --single-transaction, --routines, --events, --hex-blob работают так же; вместо --set-gtid-purged у MariaDB свой механизм GTID.



