PR
DEV

Automatically Back Up WordPress with `rsync wp db export` [Mac × launchd]

Is it really safe to rely entirely on plugins for WordPress backups? While UpdraftPlus and BackWPup are convenient, as your site grows, they may stop due to timeouts, or plugin updates might break your settings.

In this article,rsync and wp db export to create a “dual safety net” locally by running a full automatic WordPress backup daily via macOS’s launchd to run the backup automatically every day, and by combining this with Time Machine, you’ll create a “double safety net locally.”

Based on scripts I actually use on both a VPS (KUSANAGI 9) and a shared server, I’ve compiled ready-to-use content ranging from design concepts to restoration procedures.

A Plugin-Free Backup Design

There are three main methods for backing up WordPress.

  1. Using the hosting provider’s automatic backup feature
  2. Using plugins (such as UpdraftPlus or BackWPup)
  3. Setting it up yourself using command-line tools (rsync, wp-cli)

While options 1 and 2 are convenient, they come with certain limitations.

MethodAdvantagesDisadvantages
Server Automatic BackupNo configuration required; runs automatically on the serverRetention period is limited (typically 7–14 days). Data may be lost in the event of a server failure
PluginsCan be managed via the admin panel. Easy to integrate with cloud servicesProne to timeouts on large-scale sites. Risk of settings being corrupted by plugin updates
rsync wp-cliLow server load. Fast due to incremental transfers. Retention rules can be freely customizedRequires terminal operations. Initial setup is somewhat time-consuming

The biggest advantage of backups using rsync and WP-CLI is that the backup data is stored on your local machine. Even if the entire server goes down, the data remains on your local machine. Unlike plugins, you are not constrained by PHP execution time limits.

You can set up this same system not only if you manage your own VPS but also on a shared hosting server, provided you have SSH access and can use WP-CLI.

Overall Design and Required Environment

Overview of the Backup

The overall flow of the backup system we’ll be setting up is as follows:

  1. Export the database on the server — wp db export to generate an SQL dump,gzip compress it
  2. Synchronize locally via rsync — Download the file and DB dump to the Mac using incremental transfer
  3. Automatically run daily via launchd — Runs automatically at a set time every day as long as the Mac is running
  4. Time Machine provides double protection — Time Machine backs up the locally stored backup

The key design principle is “only export the database on the server side” and “pull all file transfers from the local machine.” This eliminates the need to set up a backup cron job on the server, simplifying management.

Prerequisites

  • You must be able to use the Mac Terminal
  • Ability to log in to the server using SSH public key authentication (it works with password authentication as well, but key-based authentication without a passphrase is required for automation)
  • WP-CLI must be installed on the server
  • rsync must be installed locally (included by default on macOS)

The basics of using WP-CLI are explained in “How to Use WP-CLI: Commands to Dramatically Streamline WordPress Management.”

If you haven’t configured your SSH connection yet,~/.ssh/config setting up a hostname alias here will make the script easier to read.

# ~/.ssh/config の設定例
Host my-vps
    HostName 203.0.113.10
    User kusanagi
    Port 22
    IdentityFile ~/.ssh/id_ed25519

Overview of the Backup Script

From here, I’ll explain a backup script you can actually use. I’ve prepared two versions: one for VPS (KUSANAGI 9) and one for shared hosting. The design philosophy is the same for both.

Script for VPS (KUSANAGI 9)

In KUSANAGI 9, WordPress profiles are /home/kusanagi/ . This script automatically detects the profile and backs up the entire system configuration and WordPress data.

#!/bin/zsh

# --- 設定項目 ---
VPS_HOST="my-vps"                    # SSH configで定義したホスト名
BACKUP_ROOT="$HOME/Backups/VPS_Full" # ローカルの保存先

# macOS通知用の関数
notify() {
    local title=$1
    local message=$2
    osascript -e "display notification "$message" with title "$title""
}

echo "============================================"
echo "KUSANAGI 9 Auto-Detection Backup Started"
echo "============================================"

# -----------------------------------------------
# 1. システム設定ファイルの同期
# -----------------------------------------------
echo "[1/3] Syncing System Configurations..."
SYSTEM_DIRS=(
    "/etc/opt/kusanagi/"    # Nginx / PHP の設定
    "/etc/my.cnf.d/"        # MariaDB設定
    "/var/spool/cron/"      # Cron設定
    "/root/"                # rootユーザー設定
)

for S_DIR in "${SYSTEM_DIRS[@]}"; do
    mkdir -p "$BACKUP_ROOT$(dirname $S_DIR)"
    if ! rsync -avz --delete $VPS_HOST:"$S_DIR" "$BACKUP_ROOT$S_DIR" > /dev/null 2>&1; then
        notify "バックアップエラー" "システム設定 ($S_DIR) の同期に失敗しました"
    fi
done

# -----------------------------------------------
# 2. WordPressプロファイルの自動検出
# -----------------------------------------------
echo "[2/3] Detecting WordPress profiles..."
PROFILES=($(ssh $VPS_HOST "ls -F /home/kusanagi/ | grep '/$' | sed 's//$//'
    | grep -vE '^(backups|php|kusanagi)$'" 2>/dev/null))

if [ ${#PROFILES[@]} -eq 0 ]; then
    notify "バックアップエラー" "プロファイルが見つかりませんでした"
    exit 1
fi

echo "  Found profiles: ${PROFILES[@]}"

# -----------------------------------------------
# 3. 各プロファイルのDBエクスポート
# -----------------------------------------------
echo "[3/3] Exporting databases..."
for PROFILE in "${PROFILES[@]}"; do
    echo "  DB export for: $PROFILE"
    ssh $VPS_HOST "if [ -d /home/kusanagi/$PROFILE/DocumentRoot ]; then 
        nice -n 19 wp db export --allow-root 
            --path=/home/kusanagi/$PROFILE/DocumentRoot 
            /home/kusanagi/$PROFILE/db_backup.sql > /dev/null && 
        gzip -f /home/kusanagi/$PROFILE/db_backup.sql; 
    fi"
done

# -----------------------------------------------
# 4. 全データの一括同期
# -----------------------------------------------
echo "Syncing all profile data..."
mkdir -p "$BACKUP_ROOT/home/kusanagi"
rsync -avz --delete --force --delete-excluded 
    --exclude='backups' 
    --exclude='php' 
    --exclude='kusanagi' 
    --exclude='*/wp-content/cache/*' 
    --exclude='*/wp-content/uploads/kusanagi_html/*' 
    $VPS_HOST:/home/kusanagi/ "$BACKUP_ROOT/home/kusanagi/"

RC=$?

if [ $RC -eq 0 ] || [ $RC -eq 24 ]; then
    notify "バックアップ完了" "全データを正常に同期しました"
    echo "  [OK] Sync completed successfully"
else
    notify "バックアップエラー" "ファイル同期に失敗しました (エラーコード: $RC)"
    echo "  [ERROR] Sync failed (Exit code: $RC)"
fi

echo "============================================"
echo "Backup Process Finished"
echo "============================================"

Here are the key points of the script.

  • nice -n 19 It sets the priority of the DB export to the lowest level to minimize impact on the live site
  • rsync --delete Reflects files deleted on the server locally (full mirror)
  • --exclude Excludes unnecessary data such as cache files to reduce data transfer
  • gzip -f Compresses the SQL dump to reduce data transfer
  • Exit code 24(Some files were deleted during transfer) is treated as a successful completion

Script for shared hosting servers

This script is designed for shared servers (environments where SSH connections and WP-CLI are available). It automatically detects domains and subdomains and backs them up.

#!/bin/zsh

# --- 設定項目 ---
SERVER_HOST="my-server"               # SSH configで定義したホスト名
BACKUP_ROOT="$HOME/Backups/Server_Full"
REMOTE_HOME="/home/username"          # リモートのホームディレクトリ

notify() {
    local title=$1
    local message=$2
    osascript -e "display notification "$message" with title "$title""
}

echo "============================================"
echo "Server Backup Started"
echo "============================================"

# ドメインディレクトリの自動検出
DOMAINS=($(ssh $SERVER_HOST 
    "ls -1 $REMOTE_HOME/ | grep -E '.(org|net|com|jp)$'" 2>/dev/null))

if [ ${#DOMAINS[@]} -eq 0 ]; then
    notify "バックアップエラー" "ドメインが見つかりませんでした"
    exit 1
fi

echo "Found domains: ${DOMAINS[@]}"

WP_COUNT=0

for DOMAIN in "${DOMAINS[@]}"; do
    echo "Processing: $DOMAIN"
    DOMAIN_PATH="$REMOTE_HOME/$DOMAIN"
    PUBLIC_HTML="$DOMAIN_PATH/public_html"

    # WordPress検出 → DBエクスポート
    process_wp_site() {
        local wp_path=$1
        local backup_dir=$2
        local site_name=$3

        echo "  [WP] DB export for $site_name..."
        ssh $SERVER_HOST "if [ -f '$wp_path/wp-config.php' ]; then 
            mkdir -p '$backup_dir' && 
            nice -n 19 wp db export 
                --path='$wp_path' 
                '$backup_dir/db_backup.sql' 2>/dev/null && 
            gzip -f '$backup_dir/db_backup.sql' 2>/dev/null; 
        fi"
        ((WP_COUNT  ))
    }

    # メインドメインのチェック
    ssh $SERVER_HOST "[ -f '$PUBLIC_HTML/wp-config.php' ]" 2>/dev/null
    if [ $? -eq 0 ]; then
        process_wp_site "$PUBLIC_HTML" "$DOMAIN_PATH/backup" "$DOMAIN"
    fi

    # サブドメインの自動検出
    SUBDOMAINS=($(ssh $SERVER_HOST 
        "ls -1 '$PUBLIC_HTML' 2>/dev/null 
        | grep -E '.[a-z] $'" 2>/dev/null))

    for SUB in "${SUBDOMAINS[@]}"; do
        SUB_PATH="$PUBLIC_HTML/$SUB"
        ssh $SERVER_HOST "[ -f '$SUB_PATH/wp-config.php' ]" 2>/dev/null
        if [ $? -eq 0 ]; then
            process_wp_site "$SUB_PATH" 
                "$DOMAIN_PATH/backup/$SUB" "$SUB"
        fi
    done
done

# 全データ一括同期
echo "Syncing all data..."
mkdir -p "$BACKUP_ROOT$REMOTE_HOME"
rsync -avz --delete --force --delete-excluded 
    --exclude='*/wp-content/cache/*' 
    $SERVER_HOST:$REMOTE_HOME/ "$BACKUP_ROOT$REMOTE_HOME/" > /dev/null 2>&1

RC=$?

if [ $RC -eq 0 ] || [ $RC -eq 24 ]; then
    notify "バックアップ完了" 
        "${#DOMAINS[@]}ドメイン (WP: $WP_COUNT) を同期しました"
else
    notify "バックアップエラー" 
        "同期に失敗しました (エラーコード: $RC)"
fi

echo "============================================"
echo "Backup Process Finished"
echo "============================================"

Design of rsync exclusion rules

rsync --exclude options must be configured appropriately depending on the environment. The following are typical items to exclude.

Items to ExcludeReason
*/wp-content/cache/*Files generated by cache plugins. Unnecessary because they are regenerated
*/wp-content/uploads/kusanagi_html/*KUSANAGI page cache. Not needed because it is regenerated
*/wp-content/debug.logDebug logs. Not necessary for backup
*.tmp / *.swpTemporary files. No need to back up

On the other hand, there are some items that must never be excluded.

  • wp-content/uploads/ — All media files. If these are lost, all images in your posts will disappear
  • wp-content/themes/ — Custom theme source code
  • wp-content/plugins/ — Plugins and their settings
  • wp-config.php — Critical files such as database connection information and authentication keys
  • .htaccess — Permalink and redirect settings

For more details on rsync exclusion rules, please also refer to “The Difference Between the rsync Command and the Presence/Absence of a Slash.”

Note on the DB dump destination

If you save the database export to public_html(the document root), third parties will be able to download the SQL file directly from a browser. This poses a serious security risk, as the contents of the database (including user information and password hashes) will be fully exposed.

In the script above, the files are saved outside the document root (e.g., /home/kusanagi/profile_name/db_backup.sql.gz or /home/username/example.com/backup/db_backup.sql.gz). Be sure to save it outside the web-facing directory.

Scheduled Execution via macOS launchd

While manually running the script is sufficient, you can use macOS’s launchd , you can have it run automatically every day. This is the macOS equivalent of cron .

Creating a plist file

Create a plist file with the following contents ~/Library/LaunchAgents/ .





    Label
    com.user.vpsbackup
    ProgramArguments
    
        /bin/zsh
        /Users/username/Backups/scripts/vps_backup.sh
    
    StartCalendarInterval
    
        
            Hour
            4
            Minute
            0
        
        
            Hour
            16
            Minute
            0
        
    
    RunAtLoad
    
    StandardOutPath
    /tmp/vps_backup.log
    StandardErrorPath
    /tmp/vps_backup_error.log

Name the file com.user.vpsbackup.plist ,Label .

Registering with launchd and verifying operation

# plistの文法チェック
plutil -lint ~/Library/LaunchAgents/com.user.vpsbackup.plist

# 既存登録があれば解除してから再登録
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.vpsbackup.plist 2>/dev/null
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.user.vpsbackup.plist

# 手動で即時実行してテスト
launchctl kickstart -k gui/$(id -u)/com.user.vpsbackup

# 実行ログの確認
tail -f /tmp/vps_backup.log /tmp/vps_backup_error.log

To unregister, launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.vpsbackup.plist execute the following command.

Key points for launchd configuration

  • StartCalendarInterval Defining it as an array allows it to run multiple times a day. In the example above, it runs daily at 4:00 AM and 4:00 PM
  • It will not run if the Mac is asleep or powered off. It will run the next time the system starts up
  • RunAtLoad If you set true , it will run once immediately after login
  • StandardOutPath By separating StandardErrorPath Separating `logs` into `normal` and `error` makes it easier to track them
  • Since logs are /tmp/ , so they will be cleared upon restart. If you want to persist them, ~/Library/Logs/ please change it to

Dual Protection with Time Machine

Backup data synchronized locally via rsync is automatically included in macOS Time Machine backups. This means you get the following “dual protection” without any special configuration:

  1. Layer 1: rsync — Server → Mac (daily latest snapshot)
  2. Layer 2: Time Machine — Mac → External disk (with retention policy)

The advantage of this configuration is that you can “restore to any state.” While rsync alone can only maintain the server’s current state, Time Machine preserves past states in chronological order.

For example, if you want to “revert to the database state from three days ago,” you can retrieve db_backup.sql.gz .

Restore Procedure

Backing up isn’t just about “taking the backup and calling it a day”; it’s important to also confirm the restore procedure. Here, we’ll explain the main restore scenarios.

Restoring Files

Use rsync to restore files to the server from a backup saved locally.

# まずドライランで確認(実際には転送しない)
rsync -avzn --delete 
    ~/Backups/VPS_Full/home/kusanagi/profile_name/ 
    my-vps:/home/kusanagi/profile_name/

# 問題なければ実行
rsync -avz --delete 
    ~/Backups/VPS_Full/home/kusanagi/profile_name/ 
    my-vps:/home/kusanagi/profile_name/

-nBe sure to run the (dry run) option first. This prevents accidental overwriting or deletion of files.

Database Restore

Transfer the SQL dump saved locally to the server and import it.

# 1. ダンプファイルを解凍
gunzip ~/Backups/VPS_Full/home/kusanagi/profile_name/db_backup.sql.gz

# 2. サーバーに転送
scp ~/Backups/VPS_Full/home/kusanagi/profile_name/db_backup.sql 
    my-vps:/home/kusanagi/profile_name/

# 3. サーバー上でインポート
ssh my-vps "wp db import 
    --path=/home/kusanagi/profile_name/DocumentRoot 
    /home/kusanagi/profile_name/db_backup.sql"

#  4. URLの置換が必要な場合(ドメイン変更時など)
ssh my-vps "wp search-replace 
    'https://old-domain.com' 'https://new-domain.com' 
    --path=/home/kusanagi/profile_name/DocumentRoot 
    --all-tables"

wp db import Since this will overwrite existing tables, we recommend exporting the current database before running this command.

To restore from a backup of a specific date

If you are using Time Machine, navigate to the backup directory for the relevant date and time in Finder, and copy the files from there.

# Time Machineのバックアップ先を確認
tmutil latestbackup

# 過去の日付からダンプファイルを見つける例
# Finderの「Time Machineに入る」から該当ファイルを選択するのが簡単です

Troubleshooting

rsync fails with “Permission denied”

Please check your SSH key authentication settings. When running via launchd, the script may run with different permissions than your shell environment.

# SSH接続テスト
ssh -v my-vps "echo OK"

# 鍵のパーミッション確認
ls -la ~/.ssh/id_ed25519
# → -rw------- であることを確認(600)

Script does not run via launchd

Common causes and solutions.

  • Plist syntax error — plutil -lint ファイル名.plist Check for
  • PATH is not set — If necessary, add the EnvironmentVariables to PATH to the plist
  • HOME is not set — ~ is not expanded in the launchd environment, soHOME please specify it via an environment variable
  • Mac was asleep — Set the launchd scheduled task to run during hours when the Mac is awake
# launchdの登録状態を確認
launchctl list | grep vpsbackup

# ログを確認
cat /tmp/vps_backup.log
cat /tmp/vps_backup_error.log

wp db export is not working

  • WP-CLI is not installed — ssh my-vps "wp --version" Please check
  • The path is incorrect — --path Specify the exact WordPress installation directory in the options
  • Permission issues — On a VPS, --allow-root may be required

rsync transfer is slow

  • -z Please check if the option (compressed transfer) is enabled
  • --exclude Check if caches and unnecessary files are excluded
  • The first transfer takes time because it is a full transfer, but subsequent transfers are incremental, so they are significantly faster

Frequently Asked Questions (FAQ)

How often should I run backups?

This depends on how frequently your site is updated. For sites updated daily, once a day is sufficient; for sites updated once or twice a week, two to three times a week is sufficient. Since this script uses rsync incremental transfers, server load and data transfer volume remain minimal even if run daily.

Which is better: a plugin backup or a script backup?

If technically possible, I recommend the script. Plugins are subject to PHP execution time limits, so sites with many images are prone to timeouts. On the other hand, if you are not familiar with terminal operations, a plugin like UpdraftPlus is a more reliable option. Using both together is also a valid choice.

Can I use the same method in a Windows environment?

You can set up an equivalent environment using WSL2 (Windows Subsystem for Linux). The rsync and ssh commands run within the Linux environment inside WSL2. However, you’ll need to use the Windows Task Scheduler instead of launchd.

What if I want to back up multiple servers?

If you prepare a script and a plist file for each server, they can be executed automatically and independently. By staggering the execution times, you can avoid network bandwidth conflicts.

Summary

In this article, we explained how to set up an automated WordPress backup environment using rsync and wp-db-export. Let’s review the key points.

  • Use `rsync` and `wp-db-export` to back up files and the database together to a local location
  • Save the DB dump outside the document root (a security requirement)
  • Set up daily automatic execution using macOS launchd. Manage the schedule with a plist file
  • Combine with Time Machine to achieve dual protection with version control
  • Restore using `rsync wp db import`. Run a dry run to verify before executing

By setting up a backup system that doesn’t rely on plugins, you won’t have to panic if you’re affected by server failures or plugin malfunctions. Once you configure the script, it runs automatically every day without any further action, so just put in the effort for the initial setup.

For details on deploying from local to production using rsync, see “Safe Deployment from Local to Production with wp db and rsync.” Setting up your deployment environment alongside your backup system will make your WordPress operations even safer and more efficient.

Comment

Copied title and URL