Backup files and databases automatically with cron

A good backup is not a command you copied from a forum in 2005.

A good backup is automated, readable, logged, rotated, stored away from the server, and tested before disaster strikes. Cron can still handle the scheduling part perfectly well. However, the backup command itself needs a little more care than a one-liner thrown into cPanel.

This guide shows a modern way to back up website files and MySQL or MariaDB databases with cron. It works well for small and medium websites, WordPress installs, forums, static sites and custom PHP applications.

If you run a large database, a busy WooCommerce shop or a high-write application, treat this as a baseline. You may need replica-based backups, physical backups, binary logs, snapshots or managed backup tooling.

What should a backup include?

For most websites, you need two things:

  • the website files, such as themes, plugins, uploads, media and custom code;
  • the database, which contains posts, pages, users, settings, orders, comments and application data.

Backing up only the files is not enough. Backing up only the database is not enough either. A WordPress site without its database is a cupboard with no shelves. A database without uploads is a library with missing pages.

The old one-liner approach

The old version of this tutorial used two simple cron commands: one for tar, one for mysqldump.

date=`date -I`; tar -zcf backup_$date.tgz ./public_htmlCode language: JavaScript (javascript)
date=`date -I`; mysqldump -uDBUSER -pDBPASS --all-databases | gzip > /home/CPANEL/xbackup_$date.sql.gzCode language: JavaScript (javascript)

Those commands are useful for learning the concept. They are not ideal for production.

  • The database password appears directly in the cron command.
  • The backup stays on the same server.
  • There is no retention policy.
  • There is no log file.
  • There is no restore test.
  • The files archive may include cache directories and previous backups.
  • The command becomes hard to maintain as the site grows.

So, let’s keep the simplicity of cron, but move the logic into a small shell script.

Distingo, le livret à 2%

Create a backup directory

First, create a directory for local temporary backups. This directory should not be publicly accessible from the web.

mkdir -p "$HOME/backups"
chmod 700 "$HOME/backups"Code language: JavaScript (javascript)

Do not store backups inside public_html, htdocs, www or any public web root. A database dump exposed on the web is not a backup. It is a gift basket for attackers.

Store database credentials safely

Avoid putting database passwords directly in cron. Instead, create a protected MySQL option file.

nano "$HOME/.my.cnf"Code language: JavaScript (javascript)

Add the credentials for a dedicated backup user:

[client]
user=backup_user
password=replace_with_a_long_password
host=localhost

Then lock down the file permissions:

chmod 600 "$HOME/.my.cnf"Code language: JavaScript (javascript)

The backup user should only have the privileges it needs. For many simple sites, read-oriented privileges are enough. If you dump stored routines, triggers or events, you may need additional privileges depending on your MySQL or MariaDB version and configuration.

Use mysqldump or mariadb-dump?

Use the dump tool that matches your database server.

Database serverRecommended commandNotes
MySQLmysqldumpOfficial MySQL logical backup utility.
MariaDBmariadb-dumpModern MariaDB name. mysqldump may still exist as a compatibility alias on some systems.

For a small WordPress site on MySQL, mysqldump is still fine. For MariaDB, prefer mariadb-dump when available.

Check what your server provides:

command -v mysqldump
command -v mariadb-dump

A modern cron backup script

Create a script outside the public web root:

mkdir -p "$HOME/bin"
nano "$HOME/bin/site-backup.sh"Code language: JavaScript (javascript)

Paste this version and adjust the paths at the top.

#!/usr/bin/env bash

set -Eeuo pipefail

SITE_NAME="example-site"
WEB_ROOT="$HOME/public_html"
BACKUP_ROOT="$HOME/backups"
RETENTION_DAYS="14"

DATE="$(date +%F_%H-%M-%S)"
BACKUP_DIR="${BACKUP_ROOT}/${SITE_NAME}_${DATE}"
LOG_FILE="${BACKUP_ROOT}/${SITE_NAME}_${DATE}.log"

DB_DUMP_COMMAND="mysqldump"
DB_NAME="wordpress_database"

mkdir -p "$BACKUP_DIR"

log() {
	printf '[%s] %s\n' "$(date '+%F %T')" "$*" | tee -a "$LOG_FILE"
}

cleanup_failed_backup() {
	log "Backup failed. Removing incomplete directory: ${BACKUP_DIR}"
	rm -rf "$BACKUP_DIR"
}

trap cleanup_failed_backup ERR

log "Starting backup for ${SITE_NAME}"

if ! command -v "$DB_DUMP_COMMAND" >/dev/null 2>&1; then
	log "Dump command not found: ${DB_DUMP_COMMAND}"
	exit 1
fi

if [ ! -d "$WEB_ROOT" ]; then
	log "Web root does not exist: ${WEB_ROOT}"
	exit 1
fi

log "Dumping database: ${DB_NAME}"

"$DB_DUMP_COMMAND" \
	--single-transaction \
	--quick \
	--routines \
	--triggers \
	--events \
	"$DB_NAME" | gzip -9 > "${BACKUP_DIR}/${DB_NAME}.sql.gz"

log "Archiving website files"

tar \
	--create \
	--gzip \
	--file "${BACKUP_DIR}/${SITE_NAME}_files.tar.gz" \
	--directory "$WEB_ROOT" \
	--exclude='./cache' \
	--exclude='./wp-content/cache' \
	--exclude='./wp-content/uploads/cache' \
	--exclude='./wp-content/upgrade' \
	--exclude='./wp-content/debug.log' \
	.

log "Creating checksum file"

(
	cd "$BACKUP_DIR"
	sha256sum ./* > SHA256SUMS
)

log "Removing local backups older than ${RETENTION_DAYS} days"

find "$BACKUP_ROOT" \
	-mindepth 1 \
	-maxdepth 1 \
	-type d \
	-name "${SITE_NAME}_*" \
	-mtime "+${RETENTION_DAYS}" \
	-exec rm -rf {} +

find "$BACKUP_ROOT" \
	-mindepth 1 \
	-maxdepth 1 \
	-type f \
	-name "${SITE_NAME}_*.log" \
	-mtime "+${RETENTION_DAYS}" \
	-delete

log "Backup completed successfully: ${BACKUP_DIR}"

trap - ERRCode language: PHP (php)

Make the script executable:

chmod 700 "$HOME/bin/site-backup.sh"Code language: JavaScript (javascript)

Then run it manually before adding it to cron:

"$HOME/bin/site-backup.sh"Code language: JSON / JSON with Comments (json)

If the manual run fails, fix that first. Cron will not magically make a broken command better. Cron is punctual, not merciful.

For MariaDB, switch the dump command

If your server runs MariaDB and provides mariadb-dump, change this line:

DB_DUMP_COMMAND="mysqldump"Code language: JavaScript (javascript)

to:

DB_DUMP_COMMAND="mariadb-dump"Code language: JavaScript (javascript)

If you later restore the dump on a different system, test compatibility first. Recent MariaDB dump files may include MariaDB-specific directives that older clients or MySQL clients do not understand.

Back up all databases or one database?

The old tutorial used --all-databases. That can still make sense on a small VPS where you control every database.

For shared hosting or a single website, backing up only the relevant database is cleaner:

mysqldump --single-transaction --quick --routines --triggers --events wordpress_database | gzip -9 > wordpress_database.sql.gz

For all databases on a server you administer:

mysqldump --all-databases --single-transaction --quick --routines --triggers --events | gzip -9 > all-databases.sql.gz

The --single-transaction option is especially useful with InnoDB tables because it creates a consistent snapshot without locking normal reads and writes for the full duration of the dump.

Exclude cache and temporary files

Do not back up everything blindly. Cache folders can make backups huge, slow and noisy.

For WordPress, common exclusions include:

  • wp-content/cache
  • wp-content/upgrade
  • wp-content/uploads/cache
  • wp-content/debug.log
  • plugin-specific cache directories
  • previous backup archives

GNU tar supports exclusion patterns with --exclude and exclusion files with --exclude-from. For a larger setup, move exclusions into a dedicated file.

nano "$HOME/backup-excludes.txt"Code language: JavaScript (javascript)
./wp-content/cache
./wp-content/upgrade
./wp-content/uploads/cache
./wp-content/debug.log
./backup
./backups

Then call tar with:

tar --create --gzip --file files.tar.gz --directory "$WEB_ROOT" --exclude-from="$HOME/backup-excludes.txt" .Code language: JavaScript (javascript)

Add the script to cron

Open your crontab:

crontab -e

Run the backup every night at 02:30:

30 2 * * * /home/your-user/bin/site-backup.sh >> /home/your-user/backups/cron.log 2>&1Code language: JavaScript (javascript)

Use absolute paths in cron. Your interactive shell and cron do not always share the same environment, PATH or working directory.

On cPanel, use the Cron Jobs interface and paste the same command. Adjust the path to match your hosting account:

/home/cpanel-user/bin/site-backup.sh >> /home/cpanel-user/backups/cron.log 2>&1Code language: JavaScript (javascript)

Do not keep backups only on the same server

A local backup is useful for quick restores. It is not enough.

If the disk fails, the account is deleted, the server is compromised or the hosting provider has an incident, local backups can disappear with the original data.

At minimum, copy backups to another location:

  • another server via rsync over SSH;
  • object storage such as S3-compatible storage;
  • a backup server provided by your host;
  • a dedicated backup tool such as BorgBackup, Restic or Rclone.

For a simple remote copy with rsync:

rsync -az --delete "$HOME/backups/" backup-user@backup.example.com:/srv/backups/example-site/Code language: JavaScript (javascript)

Add this only after SSH keys and permissions are correctly configured. The remote backup user should have limited access to the backup directory, not full access to the server.

Check that backups can be restored

A backup you never restore is only a hopeful archive.

Test the database dump regularly on a staging machine or local environment:

gunzip -c wordpress_database.sql.gz | mysql staging_database

Test the file archive too:

tar -tzf example-site_files.tar.gz | head

Verify checksums:

cd /path/to/backup-directory
sha256sum -c SHA256SUMS

For WordPress, a real restore test means checking that the site loads, the admin works, uploads are present, permalinks work and key plugin data survived the import.

Recommended backup frequency

The right schedule depends on how often your data changes.

Site typeDatabase backupFiles backupComment
Static or brochure siteWeeklyWeeklyIncrease before updates.
Blog with regular postsDailyDaily or weeklyUploads matter when publishing.
Forum or membership siteDaily or moreDailyUser activity changes often.
WooCommerce shopHourly or managed backupsDailyOrders make daily-only backups risky.
Development siteBefore major changesBefore major changesAutomate if clients edit content.

For WooCommerce, daily backups can lose orders. Use your host’s continuous backups, database binlogs, a transactional backup service, or a custom strategy that matches your recovery point objective.

Cron or systemd timers?

Cron is still perfectly fine for simple scheduled backups. It is available almost everywhere, including shared hosting and cPanel.

On a modern VPS, systemd timers can be cleaner. They integrate with journalctl, handle missed runs better and offer more explicit service definitions. Still, cron wins when portability matters.

Use cron when you want the simplest path. Use systemd timers when you manage the whole server and want stronger observability.

Security checklist

  • Store backups outside the public web root.
  • Use chmod 600 for database credential files.
  • Use a dedicated database backup user.
  • Do not put passwords directly in cron.
  • Compress dumps, but do not rely on compression as security.
  • Encrypt off-server backups if they contain personal data.
  • Limit remote backup SSH keys.
  • Rotate old backups automatically.
  • Monitor failed cron runs.
  • Test restores regularly.

Quick troubleshooting

If the script works manually but not in cron, check these first:

  • use absolute paths everywhere;
  • check file permissions;
  • redirect cron output to a log file;
  • verify that mysqldump or mariadb-dump is in cron’s PATH;
  • check that $HOME resolves as expected;
  • confirm the backup user can read the database;
  • check available disk space before compression.

Useful commands:

df -h
du -sh "$HOME/backups"
tail -100 "$HOME/backups/cron.log"
command -v mysqldump
command -v mariadb-dumpCode language: JavaScript (javascript)

Conclusion

Cron remains a solid way to automate website backups. The mistake is not cron. The mistake is treating a backup as a one-line afterthought.

Put the logic in a script, keep credentials out of the crontab, dump the database with consistent options, exclude cache files, rotate old archives, copy backups off-server and test restoration. That gives you a backup process you can actually trust when something breaks.

Because the only backup that matters is the one that restores cleanly. Everything else is decorative gzip.

Sources and documentation

Gravatar for Matt Biscay

Je suis Matt Biscay, développeur WordPress & WooCommerce certifié chez Codeable, administrateur système et enseignant.

J’aide les entreprises à créer, optimiser et fiabiliser leurs sites WordPress avec une approche technique propre : performance, sécurité, maintenance, développement sur mesure et résolution de problèmes complexes.

Sur Skyminds, je partage des tutoriels WordPress, WooCommerce, Linux et administration système, avec des solutions testées sur des cas réels et pensées pour durer.

Découvrez mes services WordPress et WooCommerce.

Opinions