2026年11月13日
2026年11月13日
Automate WordPress Backups with cron: Daily DB and File Backup Script
This article is a localized edition of the Japanese original. Read the Japanese original
You can automate WordPress backups without a plugin by using Linux cron and a shell script. In this article you set up a script that creates the following two backups at a fixed time every day, and register it with cron.
- Database: posts, pages, settings, users and so on. Exported as an SQL file with
mariadb-dump(mysqldumpon MySQL) - Files:
wp-config.php, themes, plugins, uploaded images (wp-content) and so on. Bundled withtar
As of October 2026, the script was tested in the official MariaDB 11 Docker image (Ubuntu-based, bash 5.2): creating backups, deleting old files, and aborting when the database connection fails. It assumes a server where you can configure cron as root (or with sudo), such as a VPS or a dedicated server.
What you need
- SSH access and sudo rights on the Linux server running WordPress
- The WordPress database name (
DB_NAMEinwp-config.php) - A backup destination (for example
/var/backups/wordpress) with enough free space
Create a database user for backups
You can reuse the user from wp-config.php, but a separate user that has only the privileges needed for reading limits the damage if its password leaks. Connect to MariaDB / MySQL as an administrator and run:
CREATE USER 'wpbackup'@'localhost' IDENTIFIED BY 'a-long-hard-to-guess-password';
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, EVENT ON wordpress.* TO 'wpbackup'@'localhost';
Replace wordpress with your actual database name.
Keep the credentials in a separate file
If you put the password on the command line (-p password), other users may be able to see it with commands such as ps. Write the credentials to a file only root can read, and load it with --defaults-extra-file.
sudo nano /root/.wp-backup.cnf
[client]
user=wpbackup
password=a-long-hard-to-guess-password
host=localhost
sudo chmod 600 /root/.wp-backup.cnf
Create the backup script
Save the following as /usr/local/bin/wp-backup.sh. Adjust the five variables at the top to match your environment.
#!/bin/bash
# Back up the WordPress database and files, and delete old backups
set -euo pipefail
WP_DIR="/var/www/html" # WordPress install directory
BACKUP_DIR="/var/backups/wordpress" # Where backups are stored
DB_NAME="wordpress" # Database name (DB_NAME in wp-config.php)
DB_CNF="/root/.wp-backup.cnf" # File holding the database credentials
KEEP_DAYS=14 # Delete backups older than this many days
STAMP=$(date +%Y%m%d-%H%M%S)
umask 077 # Make created files readable by root only
mkdir -p "$BACKUP_DIR"
log() {
echo "$(date '+%F %T') $*"
}
# If the previous backup is still running, skip this run
exec 9> "$BACKUP_DIR/.lock"
flock -n 9 || { log "Previous backup still running, exiting"; exit 1; }
# On failure, remove the half-written files and log the exit code
cleanup() {
local rc=$?
if [ "$rc" -ne 0 ]; then
rm -f "$BACKUP_DIR/db-$STAMP.sql.gz" "$BACKUP_DIR/files-$STAMP.tar.gz"
log "Failed (exit code $rc)"
fi
}
trap cleanup EXIT
# mariadb-dump on MariaDB 11 and later, mysqldump on MySQL
DUMP=$(command -v mariadb-dump || command -v mysqldump)
log "Database backup started"
"$DUMP" --defaults-extra-file="$DB_CNF" \
--single-transaction --quick --no-tablespaces \
--default-character-set=utf8mb4 "$DB_NAME" \
| gzip > "$BACKUP_DIR/db-$STAMP.sql.gz"
log "File backup started"
# tar returns exit code 1 when files change while being read; treat that as a warning
tar -czf "$BACKUP_DIR/files-$STAMP.tar.gz" \
-C "$(dirname "$WP_DIR")" "$(basename "$WP_DIR")" \
|| { rc=$?; [ "$rc" -eq 1 ] || exit "$rc"; }
# Check that the compressed files are not corrupted
gzip -t "$BACKUP_DIR/db-$STAMP.sql.gz" "$BACKUP_DIR/files-$STAMP.tar.gz"
# Delete old backups
find "$BACKUP_DIR" -maxdepth 1 -type f \
\( -name 'db-*.sql.gz' -o -name 'files-*.tar.gz' \) \
-mtime +"$KEEP_DAYS" -delete
log "Done: db-$STAMP.sql.gz files-$STAMP.tar.gz"
How the script works
set -euo pipefailstops the script as soon as a command fails. Because ofpipefail, you do not end up with an "empty backup" wheremariadb-dumpfailed butgzipsucceeded--single-transactionexports a consistent snapshot of InnoDB tables without locking them. WordPress core tables use InnoDB, so you can back up without taking the site down--no-tablespacesavoids the error that MySQL 8.0.21 and later raise when a user without the PROCESS privilege runs the dumpflockprevents overlapping runs. Even if the data is large and the next scheduled time arrives before the previous backup finishes, the runs do not overlap- On failure, the half-written files are deleted and the exit code is written to the log
find -mtime +14deletes files last modified 15 or more days ago. Decide how many generations to keep withKEEP_DAYS, based on the space at the destination
If WP-CLI is available
On a server with WP-CLI, you can replace the database part with wp db export. The credentials are read from wp-config.php, so you do not need the .cnf file.
sudo -u www-data wp db export - --path=/var/www/html | gzip > "$BACKUP_DIR/db-$STAMP.sql.gz"
The - tells it to write to standard output instead of a file. www-data is the web server user (on Debian / Ubuntu); change it to match your environment.
Run it manually first
Before registering it with cron, run it by hand and check the result.
sudo chmod 700 /usr/local/bin/wp-backup.sh
sudo /usr/local/bin/wp-backup.sh
ls -l /var/backups/wordpress
On success you see output like this, and two files appear in the destination.
2026-10-29 17:29:39 Database backup started
2026-10-29 17:29:39 File backup started
2026-10-29 17:29:39 Done: db-20261029-172939.sql.gz files-20261029-172939.tar.gz
Check the contents as well.
# Database: do table creation lines appear?
zcat /var/backups/wordpress/db-*.sql.gz | grep -m 5 'CREATE TABLE'
# Files: is wp-config.php included?
tar -tzf /var/backups/wordpress/files-*.tar.gz | grep wp-config.php
If the database name or password is wrong, the script stops with an error like the following, and the half-written file is deleted.
mariadb-dump: Got error: 1044: "Access denied for user 'wpbackup'@'localhost' to database 'nodb'" when selecting the database
2026-10-29 17:29:39 Failed (exit code 2)
Register it with cron
Open root's crontab with sudo crontab -e and add the following line. It runs every day at 3:30 and appends the output to a log file.
30 3 * * * /usr/local/bin/wp-backup.sh >> /var/log/wp-backup.log 2>&1
The five fields are, from left to right, "minute hour day month weekday".
| Schedule | When it runs |
|---|---|
30 3 * * * |
Every day at 3:30 |
0 */6 * * * |
Every 6 hours (0:00, 6:00, 12:00, 18:00) |
0 4 * * 0 |
Every Sunday at 4:00 |
Instead of crontab -e, you can put a file in /etc/cron.d/. In that case, write the user to run as after the time fields.
# /etc/cron.d/wp-backup
30 3 * * * root /usr/local/bin/wp-backup.sh >> /var/log/wp-backup.log 2>&1
On Debian / Ubuntu, files in /etc/cron.d/ whose names contain . are ignored (use only letters, digits, - and _).
Things to watch when running under cron
- cron's
PATHenvironment variable is short and differs from your login shell. Write scripts and commands as full paths (paths starting with/), such as/usr/local/bin/wp-backup.sh - A
%inside a crontab line is treated as a newline. If you write something likedate +%Fdirectly in a crontab line, use\%(not needed here, because the script handles the date) - Check the server's time zone with
timedatectl. If it is UTC,30 3 * * *is 12:30 Japan time
Check that it ran
First check the script's log.
tail -n 20 /var/log/wp-backup.log
If nothing was written, check cron's own log to see whether cron started the script.
# Debian / Ubuntu
journalctl -u cron --since today
# RHEL / AlmaLinux / Rocky Linux
journalctl -u crond --since today
If rsyslog is installed, entries also go to /var/log/syslog on Debian / Ubuntu and /var/log/cron on the RHEL family.
/var/log/wp-backup.log grows over time, so add it to logrotate if you run this long-term.
Restore from a backup
Restore the database and the files from the same date and time. Take a backup of the current state before you start, to be safe.
# Restore the database (use credentials of a user with write privileges)
zcat /var/backups/wordpress/db-20261029-172939.sql.gz | sudo mariadb wordpress
# Restore the files (extracted to /var/www/html)
sudo tar -xzf /var/backups/wordpress/files-20261029-172939.tar.gz -C /var/www
On MySQL, replace mariadb with mysql. The wpbackup user created for backups has only read privileges, so it cannot be used to restore.
Caveats
- Keep copies elsewhere: if backups exist only on the same server's disk, a hardware failure or a compromise of the server destroys them too. Combine this with copies via
rsyncor to object storage, and keep them in another location as well - Disk space: on sites with a large
uploadsfolder, archiving every file daily eats space. Reduce the retention days, or use incremental backups for files (such asrsync) - Practice restoring: a backup is useful only if you can restore it. Restore it in a test environment and confirm the steps and how long it takes
- Backups contain sensitive data: they include
wp-config.phpand user information. Restrict the destination's permissions to root, and consider encryption when storing them off-site
Summary
- Export the database with
mariadb-dump(mysqldump) and--single-transaction, and archive the files withtar - Keep the credentials in a file only root can read, and load it with
--defaults-extra-file - Use
set -euo pipefail,flockand cleanup on failure to prevent empty backups and overlapping runs - Register the script in cron with full paths, and confirm runs with the script's log and
journalctl - Also store copies elsewhere and practice restoring
Related articles:
- How to back up to a remote server with rsync
- What to check when cron does not run
- How to monitor cron jobs
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)