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.
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:
Votre base de données ralentit tout ?
Tables wp_options surchargées, autoload incontrôlé, requêtes non indexées — une base WordPress mal entretenue finit toujours par plomber les temps de réponse. Je l'audite, je la nettoie, je l'optimise.
Diagnostiquons votre base de données →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 server | Recommended command | Notes |
|---|---|---|
| MySQL | mysqldump | Official MySQL logical backup utility. |
| MariaDB | mariadb-dump | Modern 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/cachewp-content/upgradewp-content/uploads/cachewp-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
rsyncover 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 type | Database backup | Files backup | Comment |
|---|---|---|---|
| Static or brochure site | Weekly | Weekly | Increase before updates. |
| Blog with regular posts | Daily | Daily or weekly | Uploads matter when publishing. |
| Forum or membership site | Daily or more | Daily | User activity changes often. |
| WooCommerce shop | Hourly or managed backups | Daily | Orders make daily-only backups risky. |
| Development site | Before major changes | Before major changes | Automate 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 600for 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
mysqldumpormariadb-dumpis in cron’s PATH; - check that
$HOMEresolves 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
- MySQL Reference Manual: Backup and Recovery
- MySQL Reference Manual: Establishing a Backup Policy
- MySQL Reference Manual: mysqldump
- MariaDB Documentation: mariadb-dump
- GNU tar Manual: Option Summary
- SkyMinds: Backup your MySQL databases with a one-liner crontab
- SkyMinds: Sauvegarde automatique des fichiers avec Backup Manager
- SkyMinds: Sauvegarder, archiver et installer des extensions WordPress avec WP-CLI
Votre base de données ralentit tout ?
Tables wp_options surchargées, autoload incontrôlé, requêtes non indexées — une base WordPress mal entretenue finit toujours par plomber les temps de réponse. Je l'audite, je la nettoie, je l'optimise.
Diagnostiquons votre base de données →
