mysqldump nightly backup with rotation (bash)
A cron-ready bash script that dumps each MySQL or MariaDB database to its own compressed file with --single-transaction, verifies it and deletes dumps older than N days.
Drafted with AI from official documentation. Scripts were run in a test environment and repository links verified. Awaiting a human check. Did it work for you? Last edited Outdated?
What this changes
Creates /var/backups/mysql (BACKUP_DIR) and writes one DB-STAMP.sql.gz per database there, then permanently deletes .sql.gz files in that folder older than KEEP_DAYS (default 14). The databases are only read.
How to undo it
Delete an unwanted dump with rm /var/backups/mysql/DB-STAMP.sql.gz. Rotated dumps cannot be recovered, so copy them elsewhere or raise KEEP_DAYS before running.
mysqldump writes a database out as SQL statements that recreate it. For InnoDB tables, --single-transaction gives a consistent snapshot without locking the application out: it starts a transaction in REPEATABLE READ and reads everything from that moment. This script runs it once per database, compresses the result, checks the gzip file and removes old dumps.
The script
#!/usr/bin/env bash
# mysql-nightly-backup.sh - one compressed mysqldump file per database, with rotation.
# License: CC0-1.0
set -euo pipefail
BACKUP_DIR="${BACKUP_DIR:-/var/backups/mysql}"
KEEP_DAYS="${KEEP_DAYS:-14}"
DEFAULTS_FILE="${DEFAULTS_FILE:-/etc/mysql-backup.cnf}"
umask 077
mkdir -p "$BACKUP_DIR"
stamp="$(date +%Y%m%d-%H%M%S)"
failed=0
log() { printf '%s %s\n' "$(date '+%F %T')" "$*"; }
if [[ ! -r "$DEFAULTS_FILE" ]]; then
log "ERROR: cannot read $DEFAULTS_FILE"
exit 1
fi
databases="$(mysql --defaults-extra-file="$DEFAULTS_FILE" -N -B -e 'SHOW DATABASES' \
| grep -Ev '^(information_schema|performance_schema|sys|mysql)$' || true)"
if [[ -z "$databases" ]]; then
log "ERROR: no databases found (check credentials in $DEFAULTS_FILE)"
exit 1
fi
while IFS= read -r db; do
out="$BACKUP_DIR/${db}-${stamp}.sql.gz"
tmp="${out}.partial"
log "dumping $db"
if mysqldump --defaults-extra-file="$DEFAULTS_FILE" \
--single-transaction --quick \
--routines --events --triggers \
--databases "$db" | gzip -c > "$tmp" && gzip -t "$tmp"; then
mv "$tmp" "$out"
log "ok $out ($(du -h "$out" | cut -f1))"
else
rm -f "$tmp"
log "ERROR: dump of $db failed"
failed=1
fi
done <<< "$databases"
if [[ "$failed" -eq 0 ]]; then
find "$BACKUP_DIR" -maxdepth 1 -type f -name '*.sql.gz' -mtime +"$KEEP_DAYS" -print -delete
else
log "skipping rotation because at least one dump failed"
fi
exit "$failed"
Set up a backup account
According to the manual, mysqldump needs SELECT on the tables, SHOW VIEW for views, TRIGGER for triggers, PROCESS unless you pass --no-tablespaces, EVENT for --events and global SELECT for --routines. With GTIDs on, --single-transaction also needs RELOAD or FLUSH_TABLES.
CREATE USER 'backup'@'localhost' IDENTIFIED BY 'CHANGE_ME';
GRANT SELECT, SHOW VIEW, TRIGGER, EVENT, PROCESS, RELOAD ON *.* TO 'backup'@'localhost';
Check before you run this
- Runs with administrator rights. Make sure you understand every line.
sudo install -m 600 -o root -g root /dev/null /etc/mysql-backup.cnf
sudo tee /etc/mysql-backup.cnf > /dev/null <<'EOF'
[client]
user=backup
password="CHANGE_ME"
EOF
Schedule it
Check before you run this
- Runs with administrator rights. Make sure you understand every line.
sudo install -m 750 mysql-nightly-backup.sh /usr/local/sbin/
# /etc/cron.d/mysql-backup
30 2 * * * root KEEP_DAYS=14 /usr/local/sbin/mysql-nightly-backup.sh >> /var/log/mysql-backup.log 2>&1
How it behaves
set -o pipefailmakes a failingmysqldumpfail the whole pipe, so a broken dump is never renamed to a final file.- Dumps are written to
*.partialfirst and renamed only aftergzip -tpasses. - Rotation only runs when every dump succeeded, so a run of failures never deletes your last good backups.
- System schemas and the
mysqlschema are skipped. Keep a separate list ofCREATE USER/GRANTstatements (for example fromSHOW GRANTS) to recreate accounts. --quickstreams rows instead of buffering whole tables in memory, which the manual recommends with--single-transactionfor large tables.
Only InnoDB tables are consistent with --single-transaction; MyISAM or MEMORY tables can still change during the dump. Avoid ALTER TABLE, DROP TABLE, RENAME TABLE and TRUNCATE TABLE while it runs: the manual warns they can make the dump wrong or fail.
Restore
Warning: each file contains CREATE DATABASE/USE and DROP TABLE IF EXISTS statements, so restoring it replaces the tables of that database. Restore into a test server first, or check the target with SHOW DATABASES before you run it.
gunzip -c /var/backups/mysql/shop-20260311-023000.sql.gz | mysql -u root -p
On a server with GTIDs enabled, each dump includes a SET @@GLOBAL.gtid_purged statement (the default --set-gtid-purged=AUTO). If you restore single databases into a server that already has its own GTID history, add --set-gtid-purged=OFF to the script. For recovery to a time after the dump, combine dumps with binary logs (see the point-in-time recovery article).
Official documentation
Did this work for you?
No votes yet. Be the first.
If it did not work, the report form below tells the editors what went wrong.
See also
- 0214 MySQL users and privileges: CREATE USER, GRANT and authentication plugins A MySQL account is a user name plus the host it connects from. You create it with CREATE USER, give it rights… Beginner · Step by step · Linux Article Administration · Security & access 15 Oct 2026
- 0215 mysql command-line client cheat sheet The connection options, built-in commands and everyday SQL you need in the mysql (and mariadb) command-line client, on one page. Beginner · Cheat sheet · Any system Article Administration · Queries 15 Oct 2026
- 0217 MySQL binary logs and point-in-time recovery The binary log records every change after your last backup. Restore the backup, then replay the binary log up to just before… Advanced · Explains an idea · Linux Article Backup & restore · Replication 15 Oct 2026
- 0218 Setting up MySQL source-replica replication with GTIDs Step by step: configure a MySQL 8.4 source and replica with GTIDs, create the replication account, copy the data, connect the replica… Advanced · Step by step · Linux Article Administration · Replication 15 Oct 2026
- 0220 Fix: ERROR 1045 (28000): Access denied for user ‘appuser’@’localhost’ (using password: YES) The server rejected the user, host and password combination. Usually it is a wrong password, a host part that does not match,… Beginner · Fixes an error · Linux Article Administration · Security & access 15 Oct 2026
- 0258 Build a MySQL 8.4 source-replica pair with GTIDs and rehearse a manual failover Set up GTID replication between two MySQL 8.4 servers, provision the replica with the clone plugin, then stop the source, promote the… Advanced · Guided project · Linux Article Administration · Replication 15 Oct 2026
History
Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.
- Dominik +4,801 chars