Automate WordPress Backups with WP-CLI and Cron on Hostinger VPS

by Fahim

WordPress backup plugins always seem to crap out right when your site actually needs them. The moment your database crosses 500MB or your wp-content/uploads folder hits 10GB, plugins choke. They hog PHP workers, hit memory ceilings, and lock your tables while real visitors try to load pages.

We can skip the web server overhead completely. Here is how I set up a bulletproof bash script on a Hostinger VPS that dumps the database through WP-CLI, archives site files into a compressed tarball, prunes old backups automatically, and runs nightly via cron without touching PHP limits.

Automate WordPress Backups with WP-CLI and Cron on Hostinger VPS
Automate WordPress Backups with WP-CLI and Cron on Hostinger VPS

Why Plugins Fail on Large WordPress Sites

Backup plugins live inside the WordPress runtime. Every time UpdraftPlus or All-in-One WP Migration kicks off, it runs through PHP-FPM or Apache. That means your backup is chained to every limit in your php.ini: max_execution_time, memory_limit, and FastCGI timeouts. If traffic spikes while a plugin is zipping files, your server throws a 504 Gateway Timeout and leaves a corrupt 4GB archive eating your disk space.

Running backups at the system level with WP-CLI completely bypasses the web server. WP-CLI talks straight to the local MySQL socket and the filesystem. A 1GB database export that takes 45 seconds and 512MB of RAM through a plugin finishes in under two seconds with wp db export on a Hostinger KVM VPS.

Prerequisites and WP-CLI Verification

Before writing the backup script, make sure WP-CLI is installed and accessible in your system path. Hostinger’s Ubuntu VPS templates include standard CLI tools, but WP-CLI sometimes requires a quick manual drop.

SSH into your VPS and verify WP-CLI is ready:

wp --version --allow-root

If you see wp: command not found, grab the Phar archive straight from the official WP-CLI installation handbook and make it executable globally:

curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
wp --info --allow-root

Running wp --info should spit out your version (like WP-CLI 2.10.0), PHP binary location, and config path. Also confirm your MySQL client tools are installed with mysqldump --version.

Directory Structure and Backup Storage Setup

Never, under any circumstance, store backup archives inside your web root (/var/www/html). Leaving archives in a public folder practically invites scanners to grab your raw SQL dumps, complete with user password hashes and API keys.

Create a dedicated backup directory under /var/backups/wordpress and lock down its permissions:

sudo mkdir -p /var/backups/wordpress/{daily,logs}
sudo chown -R root:root /var/backups/wordpress
sudo chmod 700 /var/backups/wordpress

This separates your daily archives and keeps logs in one place so you can spot disk exhaustion before it bites you. If you ever run into storage problems down the road, check out our guide on how to fix failed to write file to disk errors on Hostinger.

Writing the Automated Backup Shell Script

Our script needs to do four things cleanly: dump the database, archive the web root while skipping cache junk, bundle everything into a single timestamped .tar.gz, and wipe archives older than 14 days.

Create the script at /usr/local/bin/wp-backup.sh:

sudo nano /usr/local/bin/wp-backup.sh

Add the following script. Make sure you set WP_PATH to your actual site root (such as /var/www/html or /var/www/example.com):

#!/usr/bin/env bash # Exit immediately if a command exits with a non-zero status
set -euo pipefail # Configuration
SITE_NAME="mysite"
WP_PATH="/var/www/html"
BACKUP_DIR="/var/backups/wordpress/daily"
LOG_FILE="/var/backups/wordpress/logs/backup.log"
RETENTION_DAYS=14
DATE=$(date +"%Y%m%d_%H%M%S")
TEMP_DIR="/tmp/wp_backup_${DATE}"
ARCHIVE_NAME="${SITE_NAME}_backup_${DATE}.tar.gz" # Ensure output directory exists
mkdir -p "${BACKUP_DIR}"
mkdir -p "$(dirname "${LOG_FILE}")" echo "[${DATE}] Starting backup for ${SITE_NAME}..." >> "${LOG_FILE}" # Create isolated temp folder
mkdir -p "${TEMP_DIR}" # 1. Export MySQL Database via WP-CLI
echo "[$(date +"%Y%m%d_%H%M%S")] Exporting database..." >> "${LOG_FILE}"
wp db export "${TEMP_DIR}/database.sql"  --path="${WP_PATH}"  --allow-root  --add-drop-table  >> "${LOG_FILE}" 2>&1 # 2. Archive WordPress Files (excluding common cache directories)
echo "[$(date +"%Y%m%d_%H%M%S")] Archiving web files..." >> "${LOG_FILE}"
tar -czf "${TEMP_DIR}/files.tar.gz"  -C "${WP_PATH}"  --exclude="wp-content/cache"  --exclude="wp-content/updraft"  --exclude="wp-content/backups"  . >> "${LOG_FILE}" 2>&1 # 3. Combine Database and Files into Final Gzip Archive
echo "[$(date +"%Y%m%d_%H%M%S")] Creating final compressed bundle..." >> "${LOG_FILE}"
tar -czf "${BACKUP_DIR}/${ARCHIVE_NAME}" -C "${TEMP_DIR}" database.sql files.tar.gz # 4. Clean up temporary directory
rm -rf "${TEMP_DIR}" # 5. Prune backups older than RETENTION_DAYS
echo "[$(date +"%Y%m%d_%H%M%S")] Rotating old archives (older than ${RETENTION_DAYS} days)..." >> "${LOG_FILE}"
find "${BACKUP_DIR}" -type f -name "${SITE_NAME}_backup_*.tar.gz" -mtime +"${RETENTION_DAYS}" -delete echo "[$(date +"%Y%m%d_%H%M%S")] Backup completed successfully: ${ARCHIVE_NAME}" >> "${LOG_FILE}"
exit 0

Make it executable and restrict access so only root can read or run it:

sudo chmod +x /usr/local/bin/wp-backup.sh
sudo chown root:root /usr/local/bin/wp-backup.sh

Testing the Script and Fixing Common Permission Gotchas

Do not wait for cron to test your script. Trigger a manual run now to ensure WP-CLI pulls your database credentials properly from wp-config.php.

Run it manually:

sudo /usr/local/bin/wp-backup.sh

Inspect the output log and confirm the archive landed in your daily folder:

cat /var/backups/wordpress/logs/backup.log
ls -lh /var/backups/wordpress/daily

On a fresh Hostinger VPS, keep an eye out for these two snags:

  • Error establishing database connection: WP-CLI runs wp-config.php using the CLI PHP runtime. If your DB_HOST is set to localhost, MySQL might look for a socket file where CLI PHP cannot find it (like /var/run/mysqld/mysqld.sock). If that happens, verify your credentials with our guide on how to fix error establishing a database connection on Hostinger.
  • Disk space exhaustion: If you have massive media uploads, building files.tar.gz inside /tmp can fill your root partition. If your /tmp is mounted on a small tmpfs partition, point TEMP_DIR inside the script to /var/backups/wordpress/tmp instead.

Scheduling Automated Nightly Backups with System Cron

Once the script runs cleanly by hand, let cron take over. I like scheduling it for 3:30 AM when traffic is at its absolute lowest.

Open the root crontab:

sudo crontab -e

Add the PATH definition and the cron entry at the bottom (check the standard crontab manual if you want a different interval):

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin # Run WordPress automated backup daily at 3:30 AM UTC
30 3 * * * /usr/local/bin/wp-backup.sh > /dev/null 2>&1

Do not skip that PATH line at the top. Cron runs with an ultra-barebones environment, and /usr/local/bin (where WP-CLI lives) is frequently missing. Without it, your backups will silently fail.

To keep the backup log file from growing out of control over months of runs, follow our guide to configure startup scripts and log rotation on a VPS.

Syncing Backups to Remote Cloud Storage with Rclone

Keeping backups on the exact same drive as your live site is asking for trouble. If the VPS disk corrupts or the hypervisor goes down, your backups vanish right alongside the site. Offload your /var/backups/wordpress/daily folder to Cloudflare R2, AWS S3, or Backblaze B2 using rclone.

Install Rclone:

sudo apt-get update && sudo apt-get install -y rclone

Run the interactive setup to link your bucket according to the Rclone documentation:

rclone config

Once you have a remote configured (named remote-storage here), add this sync line right before exit 0 in /usr/local/bin/wp-backup.sh:

# Sync local archives to remote bucket and delete old files on the remote
echo "[$(date +"%Y%m%d_%H%M%S")] Syncing to remote object storage..." >> "${LOG_FILE}"
rclone sync "${BACKUP_DIR}" remote-storage:my-wp-backups-bucket/daily  --transfers 4  >> "${LOG_FILE}" 2>&1

Because it uses sync, your off-site bucket automatically mirrors your 14-day local retention without leaving orphaned files.

Disaster Recovery: Restoring Your Site from an Archive

An untested backup is just wishful thinking. If a plugin update trashes your site, you can restore everything from the command line in under a minute.

Here is how to restore an archive step-by-step:

1. Extract the backup archive into a temp folder:

mkdir -p /tmp/restore
tar -xzf /var/backups/wordpress/daily/mysite_backup_20250520_033000.tar.gz -C /tmp/restore

2. Import the database using WP-CLI:

wp db import /tmp/restore/database.sql --path=/var/www/html --allow-root

3. Extract the web files back to your document root and reset file ownership:

tar -xzf /tmp/restore/files.tar.gz -C /var/www/html/
sudo chown -R www-data:www-data /var/www/html
sudo find /var/www/html -type d -exec chmod 755 {} ;
sudo find /var/www/html -type f -exec chmod 644 {} ;
rm -rf /tmp/restore

If you prefer testing your database import on staging before touching live traffic, check out our walkthrough on how to push WordPress staging to production without overwriting data.

Frequently Asked Questions

Why use WP-CLI instead of plain mysqldump?

WP-CLI dynamically reads your wp-config.php. You do not have to hardcode database credentials or hostnames into your shell script. Whenever you change database passwords, your backups keep running without touching the script.

Will running tar and wp db export slow down my site?

wp db export takes a brief read lock on tables, but with InnoDB tables, it finishes in a couple of seconds. Running the job in the middle of the night ensures virtually zero impact on active users.

How do I test if cron actually ran the script?

Check the log directly at /var/backups/wordpress/logs/backup.log or verify that cron executed the task by running grep CRON /var/log/syslog.

Can I back up multiple WordPress sites on the same VPS?

Yes. Just wrap the core backup logic in a for loop inside your script that iterates through an array of document roots (like /var/www/site1 and /var/www/site2), creating distinct archives for each domain.

all_in_one_marketing_tool