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.
Not checked by a trusted editor yet 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
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 · How-to · 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 · Reference · 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 · Concept · 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 · How-to · 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 · Fix · 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 · Tutorial · 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.
- Commitpedia +4,801 chars