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 (mysqldump on MySQL)
  • Files: wp-config.php, themes, plugins, uploaded images (wp-content) and so on. Bundled with tar

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_NAME in wp-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 pipefail stops the script as soon as a command fails. Because of pipefail, you do not end up with an "empty backup" where mariadb-dump failed but gzip succeeded
  • --single-transaction exports 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-tablespaces avoids the error that MySQL 8.0.21 and later raise when a user without the PROCESS privilege runs the dump
  • flock prevents 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 +14 deletes files last modified 15 or more days ago. Decide how many generations to keep with KEEP_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 PATH environment 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 like date +%F directly 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 rsync or to object storage, and keep them in another location as well
  • Disk space: on sites with a large uploads folder, archiving every file daily eats space. Reduce the retention days, or use incremental backups for files (such as rsync)
  • 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.php and 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 with tar
  • Keep the credentials in a file only root can read, and load it with --defaults-extra-file
  • Use set -euo pipefail, flock and 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:

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲