PR
DEV

How to Safely Deploy WordPress from Local to Production Using wp-db and rsync

“I want to deploy my WordPress site built locally to production. But using a plugin hits the file size limit, and I’m hesitant to use phpMyAdmin every time…” Many of you probably share this concern.

In this article, I’ll explain how to safely deploy from a local environment to a production environment using the WP-CLI `wp db` command and rsync. Since this process relies solely on the command line without plugins, there are no file size limits, and you can repeat it as many times as needed.

It might feel more daunting compared to GUI-based migration plugins, but once you understand the mechanism, it boils down to three simple steps: “Export the database → Replace the URLs → Sync the files.” Once you’ve established the workflow, future deployments will take just a few minutes.


Overall Deployment Workflow

Deployment from local to production proceeds in the following four steps.

  1. Backup the production DB (as a safety net for rollback)
  2. Export the local DB → Import to production
  3. URL replacement (wp search-replacereplace local URLs with production URLs)
  4. File synchronization with rsync (transfer themes, plugins, and uploaded images all at once)

The most critical step is Step 1: the backup. Since this operation overwrites the production database, you must ensure you have a state that allows for rollback in case of an emergency before proceeding with deployment.

Differences from plugin migration

Plugins like All-in-One WP Migration are convenient, but the free version has an import size limit, so you’ll quickly hit a wall with sites that have a lot of images. Additionally, since you have to manually open the WordPress admin panel and perform Export → Import each time, the time lost increases significantly as deployment frequency rises.

On the other hand, the combination of WP-CLI and rsync has no size limits and can be executed with a single command, which is a major advantage. Integrating this into a shell script can further streamline the deployment process.

Comparison CriteriaPlugin MigrationWP-CLI and rsync
File Size LimitFree version has a limit (typically 512MB)No limit
OperationGUI (Control Panel)CLI (Terminal)
ReproducibilityManual each timeAutomated via scripting
URL substitutionPlugin handles it automaticallywp search-replace Explicitly executed
Differential transferNot supported (full transfer)rsync transfers only the differences
Required skillsLowSSH and command-line operations

Prerequisites

Requirements

  • WordPress in a local environment (Herd, Local, Docker, etc.—any setup is fine)
  • WP-CLI (installed on both the local and production servers)
  • SSH connection (ability to log in to the production server via SSH)
  • rsync (WSL2 is recommended for Windows. The standard macOS version is outdated, so we recommend using the Homebrew version)

Use the Homebrew version of rsync on macOS

The rsync included by default on macOS is an older version (2.6 series), so you may run into issues due to option compatibility or differences in behavior. It is safer to use the newer Homebrew version of rsync.

# Homebrew が未導入なら先にインストール
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# rsync をインストール
brew install rsync

# バージョン確認(3.x ならOK)
rsync --version

If you haven’t installed Homebrew yet, follow the instructions displayed after installation PATH .which rsync The output should be /opt/homebrew/bin/rsync(Apple Silicon) or /usr/local/bin/rsync(Intel), you are using the Homebrew version.

Verifying WP-CLI Installation

Check if WP-CLI is available on both your local machine and your production environment.

# ローカル
wp --version

# 本番サーバー(SSH接続後)
wp --version --allow-root

If the version information is displayed, you’re all set. If you haven’t installed it yet, please refer to the WP-CLI user guide.

Configuring SSH Connection

An SSH connection is required for remote execution of rsync and WP-CLI. To avoid having to enter your password every time,~/.ssh/config it’s convenient to set up the connection settings in advance.

# ~/.ssh/config
Host my-server
  HostName 203.0.113.10
  User deploy
  Port 22
  IdentityFile ~/.ssh/id_ed25519

After configuring, ssh my-server verify that you can log in without a password.


Step 1: Backing Up the Production Database

Always back up your current production database before deployment. If you skip this step, you will have no way to revert changes if a problem occurs.

# 本番サーバーでDBをエクスポート(SSHログイン後)
wp db export /tmp/prod-backup-$(date  %Y%m%d%H%M%S).sql --path=/var/www/html

--path=/var/www/html This specifies “which WordPress installation the command should be executed on.”wp-config.php refers to the WordPress root directory. Since it does not depend on the current directory immediately after logging in via SSH, it makes it less likely to accidentally manipulate the wrong site.

Using filenames with timestamps prevents multiple backups from getting mixed up.

It is also possible to run the command remotely without logging in via SSH.

# ローカルからSSH経由でリモート実行
ssh my-server "wp db export /tmp/prod-backup-$(date  %Y%m%d%H%M%S).sql --path=/var/www/html"

Step 2: Database Migration

Exporting the Local Database

wp db export Export the local database to an SQL file.

# ローカル環境で実行
wp db export /tmp/local-db.sql --path=/path/to/local/wordpress

Importing to Production

Import the exported SQL into the production server’s database. There are two methods.

Method A: Transfer the SQL file and then import it

# SQLファイルを本番サーバーに転送
scp /tmp/local-db.sql my-server:/tmp/

# SSHログインしてインポート
ssh my-server "wp db import /tmp/local-db.sql --path=/var/www/html"

Method B: Import directly via pipe (transfer and import in one step)

# ローカルのSQLをパイプでリモートに流し込む
cat /tmp/local-db.sql | ssh my-server "wp db import - --path=/var/www/html"

Method B is more efficient since it eliminates the file transfer step. However, because no intermediate files are left behind, Method A makes it easier to troubleshoot if a problem occurs.


Step 3: URL Replacement (search-replace)

If you import the database as-is, the URLs contained in article content, widgets, and settings will remain as local URLs (e.g.,https://mysite.test). These must be replaced with the production URLs (e.g.,https://example.com).

Use wp search-replace . It is dangerous to perform this replacement directly using phpMyAdmin or the SQL REPLACE function. The WordPress database contains a large amount of serialized data (PHP objects converted to strings), and simple string replacement will corrupt the data.

# 本番サーバーで実行
wp search-replace 'https://mysite.test' 'https://example.com' 
  --all-tables 
  --precise 
  --recurse-objects 
  --path=/var/www/html

Meaning of each option

OptionDescription
--all-tablesIncludes tables created by plugins as well as standard WordPress tables
--precisePerform precise replacements on a per-column basis (to prevent incorrect replacements)
--recurse-objectsRecursively replace within serialized data

Replaces escaped URLs

The WordPress database contains locations where URLs are stored in a slash-escaped state (https://mysite.test). This applies to JSON-based Block Editor data, among other things. In addition to regular URLs, perform replacements on the escaped format as well.

# エスケープ済みURLの置換
wp search-replace 'https://mysite.test' 'https://example.com' 
  --all-tables 
  --precise 
  --path=/var/www/html

Flush the cache and permalinks

After URL replacement, flush the cache and permalinks to ensure no old values remain.

wp cache flush --path=/var/www/html
wp rewrite flush --path=/var/www/html

Step 4: File Synchronization with rsync

Once the database migration is complete, the next step is file synchronization. Using rsync allows you to transfer only the files that have changed, making subsequent syncs extremely fast.

Specify rsync rsync [options] 転送元 転送先 in that order. In the example below,/path/to/local/... is the source (local),my-server:/var/www/html/... is the destination (production).

Additionally, the behavior changes depending on whether / .themes/ As shown here, if there is a trailing slash, only the contents are synchronized,themes but if there is no trailing slash, as shown here, the entire directory is synchronized. To prevent unintended directory structure discrepancies, we recommend using a configuration where both the source and destination / .

Synchronizing Themes

rsync -az --delete 
  -e "ssh" 
  /path/to/local/wp-content/themes/ 
  my-server:/var/www/html/wp-content/themes/

Plugin Synchronization

rsync -az --delete 
  -e "ssh" 
  /path/to/local/wp-content/plugins/ 
  my-server:/var/www/html/wp-content/plugins/

Synchronizing Uploaded Images

rsync -az --delete 
  -e "ssh" 
  --exclude='cache/' 
  /path/to/local/wp-content/uploads/ 
  my-server:/var/www/html/wp-content/uploads/

Key rsync Options

OptionsMeaning
-aArchive mode. Recursively copies files while preserving permissions and timestamps
-zCompress during transfer (effective on slow connections)
--deleteDelete files from the destination that do not exist on the source (mirroring)
-e "ssh"Transfer via SSH
--excludeExclude specified patterns
--dry-runPreview what will happen without actually transferring

Be sure to check with `--dry-run` on your first run.--delete Because options are enabled, specifying an incorrect path will delete the actual files.

# まず dry-run で差分を確認
rsync -az --delete --dry-run 
  -e "ssh" 
  /path/to/local/wp-content/themes/ 
  my-server:/var/www/html/wp-content/themes/

# 問題なければ --dry-run を外して実行

Items that must never be synced

When syncing files with rsync, there are certain files and directories that must not be included in the sync. If these are transferred by mistake, the production environment may stop working or security risks may arise.

ItemsReason
wp-config.phpDB connection information and authentication keys differ between local and production environments. Overwriting them will cause the production environment to stop working
.htaccessMay contain server-specific rewrite rules
wp-content/cache/There is no point in transferring local cache plugin data to the production environment
wp-content/upgrade/Temporary files for updates
object-cache.phpDrop-in files for object caching. Settings vary by environment
advanced-cache.phpPage cache drop-in files. Same as above
db.phpDB drop-in files. Same as above
debug.logDebug logs. There is no need to transfer local logs to the production environment

Exclude these --exclude , or synchronize only the subdirectories under the WordPress root directory wp-content/ .

It is safer to limit file synchronization wp-content/ to the following: the WordPress core files (wp-admin/、wp-includes/) and files directly under the root directory should wp core update .


A Safe Deployment Workflow via Staging

While deploying directly from your local machine to production is convenient, we recommend using a staging environment for sites that are live.

Recommended Workflow

  1. Local → Staging: Perform the DB migration and rsync described above on the staging server
  2. Test on staging: Check for layout issues, broken links, form submissions, and plugin behavior
  3. Staging → Production: If no issues are found, apply the changes to production using the same procedure

It is easy to set up a staging environment using a subdomain (e.g.,staging.example.com). If using a VPS, you can set it up simply by adding an Nginx virtual host.

Rollback Procedure

If issues are found after deployment, restore using the backup obtained in Step 1.

# バックアップからDBを復元
ssh my-server "wp db import /tmp/prod-backup-20260411120000.sql --path=/var/www/html"

# キャッシュをクリア
ssh my-server "wp cache flush --path=/var/www/html"

If you need to revert files, simply check them out if they are managed with Git. If you are not using Git, it is a good idea to back up the files before running rsync.


Recommendation for Shell Scripting

Typing out these commands manually every time is not practical. By consolidating them into a shell script, you can complete the deployment with a single command.

Key Points for Script Design

Here are the key points to keep in mind when creating your own deployment script.

  • Separate Environment Variables into an External File: Write production hostnames, paths, and URLs .env in a file, keeping them separate from the main script. Design the script so that authentication credentials are not committed to Git
  • Confirmation prompt for production pushes: Include a “Are you sure you want to proceed?” confirmation before deploying to production. This prevents accidental execution
  • Automatic Backups: Incorporate a process to automatically back up the production database before deployment
  • Component-by-component execution: Design the system to allow separate synchronization of the database, themes, plugins, and uploads. There are many situations where transferring everything at once is unnecessary
  • Replacement of escaped URLs: Automatically perform replacements not only for standard URLs but also for URLs in escaped format (//)
  • Dry-run support: Configure the system so that rsync runs in dry-run mode when DRY_RUN=1 , rsync runs in dry-run mode

The following is a simplified example of the script’s basic structure.

#!/usr/bin/env bash
set -euo pipefail

# --- 設定 ---
PROD_HOST="my-server"
PROD_PATH="/var/www/html"
PROD_URL="https://example.com"
LOCAL_PATH="/path/to/local/wordpress"
LOCAL_URL="https://mysite.test"

# --- 本番push時の確認 ---
echo "[WARN] 本番環境にデプロイします。"
read -r -p "続行しますか? [y/N] " confirm
[[ "${confirm}" != "y" ]] && echo "中止しました。" && exit 1

# --- 本番DBバックアップ ---
TIMESTAMP=$(date  %Y%m%d%H%M%S)
ssh "${PROD_HOST}" "wp db export /tmp/prod-backup-${TIMESTAMP}.sql --path='${PROD_PATH}'"

# --- ローカルDBエクスポート → 本番インポート ---
wp db export /tmp/local-db.sql --path="${LOCAL_PATH}"
cat /tmp/local-db.sql | ssh "${PROD_HOST}" "wp db import - --path='${PROD_PATH}'"

# --- URL置換 ---
ssh "${PROD_HOST}" "wp search-replace '${LOCAL_URL}' '${PROD_URL}' --all-tables --precise --recurse-objects --path='${PROD_PATH}'"
ssh "${PROD_HOST}" "wp cache flush --path='${PROD_PATH}'"
ssh "${PROD_HOST}" "wp rewrite flush --path='${PROD_PATH}'"

# --- ファイル同期 ---
rsync -az --delete 
  -e "ssh" 
  "${LOCAL_PATH}/wp-content/themes/" 
  "${PROD_HOST}:${PROD_PATH}/wp-content/themes/"

echo "デプロイ完了"

The above is merely a skeleton. In actual production use, you will need to add exclude rules, perform double replacement of escaped URLs, implement error handling, and more.

Simplify operations with a Makefile

To make the script even easier to use, you can wrap it in a Makefile.make push-db、make push-theme Being able to run it on a per-component basis, as shown above, makes daily deployments significantly easier.

# --- Makefile の例 ---
push-db:
	@bash sync.sh local prod db

push-theme:
	@bash sync.sh local prod theme

push-all:
	@bash sync.sh local prod all

pull-db:
	@bash sync.sh prod local db

make push-db Use `db-sync` for database synchronization,make push-theme and syncing just the theme, allowing you to naturally switch between them. If you also set up a pull-type process in the opposite direction (production → local), you can fetch production data to your local environment with a single command.


Overview of Automated Deployment with GitHub Actions

To further evolve your operations, you can automate deployments using GitHub Actions.main You can set up a system that automatically applies theme files to production whenever a push is made to a branch.

Basic Workflow

  1. Modify the theme locally and push to GitHub
  2. GitHub Actions are triggered
  3. Within the workflow, rsync , use to transfer the theme files to production via SSH
  4. Deployment complete

Register the SSH private key and host information in GitHub Secrets, and add the rsync command to the .yml file.

# .github/workflows/deploy.yml(簡易例)
name: Deploy Theme
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy via rsync
        env:
          SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          rsync -az --delete 
            -e "ssh -o StrictHostKeyChecking=no" 
            ./themes/my-theme/ 
            ${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}:/var/www/html/wp-content/themes/my-theme/

However, it is safer not to automate database synchronization. Overwriting a database has a wide-ranging impact, so this step should include manual verification. We recommend limiting automation in GitHub Actions to file synchronization for themes, plugins, and similar items.


Troubleshooting

Images do not display after URL replacement

The image URL may still be pointing to the local file.wp search-replace Run --dry-run to check if any items remain to be replaced.

wp search-replace 'https://mysite.test' 'https://example.com' --all-tables --dry-run --path=/var/www/html

If the number of replacements is not 0, run the process again to resolve any omissions. It is also common to forget to replace escaped URLs.

Unable to log in to the admin panel

The production password may have changed because it was overwritten with local user information. You can reset the password using WP-CLI.

ssh my-server "wp user update admin --user_pass='new-password' --path=/var/www/html"

“Permission denied” error occurs with rsync

Check that SSH key authentication is configured correctly,~/.ssh/config the hostname and port number. Also, check whether the file owner on the production server matches the user running rsync. In KUSANAGI environments, the file owner is kusanagi user, so you must connect to rsync using that user.

Permalinks are not working (404 error)

wp rewrite flush to regenerate the permalinks. If the issue persists, check the Nginx configuration to ensure the try_files is configured correctly in the Nginx configuration. For Apache, .htaccess check the RewriteRule.


Frequently Asked Questions

How can I test without affecting the production environment?

The best approach is to set up a staging environment. If you build it on a subdomain of the same server as the production environment, you can test under the same infrastructure conditions.wp search-replace By replacing the URL with a staging version, you can create a separate testing environment.

What about plugin licenses?

Many paid plugins manage licenses on a domain-by-domain basis. Even if you deploy from a local environment, it’s fine as long as the license is activated on the production domain. Please check with the plugin provider regarding the staging domain.

Is it possible to bring production data to my local machine?

Yes, it is possible. Simply reverse the steps outlined in this article: Export the database from production → Import it to your local machine → Replace the URLs (production URL → local URL), and run rsync from production to local.

Does this work in a multisite environment?

wp search-replace Yes, it supports multisite. However,--url there are additional considerations, such as the need to specify sites via options. In the case of multisite, please thoroughly test it in a test environment beforehand.


Summary

By using wp db commands and rsync, you can achieve highly reproducible deployments without relying on plugins.

  • DB Migration:wp db export → wp db import → wp search-replace
  • File Synchronization: Transfer differences using rsync.--dry-run Always verify beforehand
  • Safety measures: A production backup before deployment is mandatory. Using a staging environment adds an extra layer of security
  • Efficiency: Automate with shell scripts → Simplify operations with a Makefile → Automate theme deployment with GitHub Actions

At first glance, it may seem like a lot of commands, but once you’ve scripted it, make push-all it can be completed with a single command like this. If you’ve been struggling with plugin limitations, please give this method a try.

If you haven’t set up a local development environment yet, check out “The Optimal Solution for a WordPress Local Development Environment” for a comparison of tools, and “Build a WordPress Local Environment in 5 Minutes with Herder and DBngin” for specific setup instructions. If you’re interested in HMR development for themes, also take a look at “Implementing HMR for WordPress Themes with Vite.”

Comment

Copied title and URL