WP-CLI Cheatsheet
What it is
WP-CLI is the command-line interface for WordPress — every action you can perform in the wp-admin dashboard (install plugins, update core, edit options, run a multisite network) has a wp subcommand that does the same thing without a browser. For deployment workflows that means treating WordPress like any other PHP app: install plugins from composer.json, swap URLs after a database restore, flush caches in a post-deploy hook, and never SSH into a host to "just fix this one option" again.
This sheet covers the commands and flags you reach for when wiring WordPress into a CI/CD pipeline — the search-replace and db export patterns that survive a staging-to-production migration, the plugin/theme update flows that don't require a browser session, and the cache discipline that keeps a multi-environment WordPress fleet predictable.
Quick reference
Installing WP-CLI and sanity checks
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 # global install
wp --info # PHP version, MySQL, WP root
wp cli version # WP-CLI version
wp cli update # self-update
wp cli check-update # check without installing
wp doctor list # diagnostics (needs wp-cli/doctor-command)
Useful environment defaults — set once per server or per CI step:
export WP_CLI_PHP=/usr/bin/php8.2 # pin a specific PHP binary
export WP_CLI_PHP_ARGS="-d memory_limit=512M" # increase memory for bulk ops
export WP_CLI_ALLOW_ROOT=1 # CI runs as root — opt in explicitly
Site bootstrap
wp core download # download WP into the cwd
wp core download --version=6.5.4 --locale=en_GB
wp config create \
--dbname=app --dbuser=app --dbpass=secret --dbhost=db --dbprefix=wp_
wp core install \
--url=example.com --title="My Site" \
--admin_user=admin --admin_email=team@example.com --admin_password=PASSWORD --skip-email
wp core is-installed # exit 0 if installed (CI-friendly)
wp core verify-checksums # detect tampered core files
Users
wp user list # interactive table
wp user list --role=administrator --field=user_email --format=csv
wp user create alice alice@example.com --role=editor --user_pass=PASSWORD --send-email=false
wp user update 42 --user_pass="$NEW_PASSWORD"
wp user delete 42 --reassign=1 # reassign content to user 1
wp user one-time-login alice # one-shot magic link (debugging only)
Plugins
wp plugin list # name, status, version, update
wp plugin list --status=active --field=name
wp plugin install wp-super-cache --activate
wp plugin install https://example.com/plugin.zip --activate
wp plugin install /path/to/plugin.zip --activate # from local file
wp plugin update --all --quiet
wp plugin update wp-super-cache --version=1.13.0
wp plugin activate wp-super-cache
wp plugin deactivate wp-super-cache
wp plugin delete wp-super-cache
wp plugin is-installed wp-super-cache # exit 0 if installed
wp plugin path wp-super-cache # absolute path
Themes
wp theme list
wp theme install twentytwentyfour --activate
wp theme install /path/to/theme.zip --activate
wp theme update --all
wp theme activate my-child-theme
wp theme delete inactive-theme
wp theme mod list # current customizer settings
wp theme mod set background_color FFFFFF
Posts, pages, and taxonomies
wp post list --post_type=page --field=ID
wp post list --post_status=publish --posts_per_page=20 --format=table
wp post create --post_type=post --post_title="Release notes" --post_status=publish \
--post_content="$(cat changelog.md)"
wp post update 42 --post_status=draft
wp post delete 42 --force # bypass trash
wp post meta set 42 _custom_field "value"
wp term list category --field=slug
wp term create category "Releases" --slug=releases --description="Release notes"
Options, transients, and rewrite rules
wp option get siteurl
wp option get blogname
wp option update blogname "Production"
wp option update siteurl "https://example.com"
wp option update home "https://example.com"
wp option pluck rewrite_rules # dump nested option values
wp transient delete --all # nuke all transients
wp transient delete some_key
wp rewrite flush # rebuild permalink rules
wp rewrite structure '/%postname%/' # set permalink structure
Database
wp db check # mysqlcheck
wp db optimize
wp db repair
wp db size --tables
wp db tables # list with prefix
wp db query "SELECT COUNT(*) FROM wp_posts WHERE post_status='publish';"
wp db export backup.sql # mysqldump → file
wp db export - | gzip > backup.sql.gz # streamed/compressed
wp db export --tables=wp_posts,wp_postmeta posts.sql
wp db import backup.sql
wp db reset --yes # DROP + CREATE all tables
search-replace (the one you'll use most)
wp search-replace 'http://staging.example.com' 'https://example.com'
wp search-replace 'old.example.com' 'new.example.com' --dry-run
wp search-replace 'old' 'new' --all-tables-with-prefix
wp search-replace 'old' 'new' --skip-columns=guid # NEVER touch guid (it's an identifier)
wp search-replace 'old' 'new' --precise # slower but correct for serialized PHP
wp search-replace 'old' 'new' --export=migrated.sql # dump replacement, don't write DB
wp search-replace handles serialized PHP correctly — a plain sed on a mysqldump corrupts every serialized array length prefix and breaks the site. Always use this for URL migrations.
Cache and OPcache
wp cache flush # object cache (Redis/Memcached)
wp cache flush-group some_group # WP 6.x+
wp transient delete --all
wp rewrite flush
wp eval 'if (function_exists("opcache_reset")) opcache_reset();' # PHP OPcache
wp eval 'echo wp_get_environment_type();' # production/staging/development
Multisite
wp site list --field=url
wp site create --slug=blog --title="Marketing" --email=team@example.com
wp site delete 3 --yes
wp site activate 3
wp site deactivate 3
wp site option update 3 blogname "Marketing"
wp super-admin list
wp super-admin add admin
# Run a command across every subsite
for url in $(wp site list --field=url); do
wp --url="$url" plugin update --all
done
Cron events
wp cron event list
wp cron event run --due-now
wp cron event run my_custom_hook
wp cron event delete my_custom_hook
wp cron test # verify wp-cron reachability
For production: disable DISABLE_WP_CRON in wp-config.php and call wp cron event run --due-now from a real cron job — see the Cron and Crontab cheatsheet for the schedule pattern.
REST API and roles
wp rest list --format=table # endpoints + capabilities
wp role list
wp role create editor_lite "Editor Lite" --clone=editor
wp cap list editor_lite
wp cap add editor_lite manage_options
Deployment workflows (the moat)
1. URL migrations after a staging-to-production restore
Restoring a staging database into production is a common deploy pattern (or recovery pattern after a botched release) — and every internal URL needs swapping before the site is usable. Don't do this with sed; serialised PHP corrupts.
#!/usr/bin/env bash
set -euo pipefail
# 1. Pull the staging dump
ssh staging "wp db export - --add-drop-table" | gunzip > /tmp/staging.sql
# 2. Restore on production (after a backup — see workflow 2)
wp db reset --yes
wp db import /tmp/staging.sql
# 3. Swap URLs — dry-run first
wp search-replace 'https://staging.example.com' 'https://example.com' \
--all-tables-with-prefix --skip-columns=guid --dry-run
# 4. Real swap if dry-run looked sane
wp search-replace 'https://staging.example.com' 'https://example.com' \
--all-tables-with-prefix --skip-columns=guid
# 5. Flush caches
wp cache flush
wp rewrite flush
wp transient delete --all
Why --skip-columns=guid: the guid column is meant to be a permanent identifier for each post — RSS readers and external systems use it as a dedupe key. Rewriting it breaks subscriptions in ways that take weeks to notice.
Why --all-tables-with-prefix: plugins routinely serialise URLs into wp_options AND into their own tables (wp_yoast_seo_links, wp_woocommerce_*). The default wp search-replace only walks the core tables — --all-tables-with-prefix walks every table with the configured prefix, which is what you actually want.
2. Pre-deploy database export as an atomic gate
Before any deploy that touches schema or runs a migration, take a snapshot — and make sure it actually wrote:
#!/usr/bin/env bash
set -euo pipefail
BACKUP="/var/backups/wp/$(date -u +%Y%m%dT%H%M%SZ).sql.gz"
mkdir -p "$(dirname "$BACKUP")"
# Export streamed through gzip — no temporary uncompressed dump on disk
wp db export - --add-drop-table | gzip -9 > "$BACKUP"
# Verify the dump is non-empty AND contains expected DDL
test -s "$BACKUP" || { echo "backup is empty" >&2; exit 1; }
gunzip -c "$BACKUP" | grep -q 'CREATE TABLE.*wp_options' \
|| { echo "backup missing wp_options DDL" >&2; exit 1; }
# Keep last 7 days
find /var/backups/wp -name '*.sql.gz' -mtime +7 -delete
echo "backup verified: $BACKUP"
The grep step catches the failure mode that bites everyone eventually: wp db export succeeds (exit 0) but produces a tiny dump because the DB credentials reference an empty database. A test -s alone doesn't catch that. Confirming a known table appears in the dump does.
For DeployHQ deploys, wire this as a pre-deploy SSH command — failing here aborts the deploy before any code changes, so production is never sitting on an "almost deployed" half-state.
3. Plugin and theme updates from CI
WordPress sites in production should not be running "Update available" prompts. Either updates flow through Composer (preferred — the vendor/ directory is the source of truth), or through WP-CLI in a deploy step. Either way, never click "Update" in wp-admin.
#!/usr/bin/env bash
set -euo pipefail
export WP_CLI_ALLOW_ROOT=1
# Snapshot DB before bulk updates — see workflow 2
/usr/local/bin/pre-deploy-backup.sh
# Core
wp core update --version=6.5.4 --quiet
wp core update-db # idempotent, safe to re-run
# All plugins, exclude maintenance-mode-sensitive ones
wp plugin update --all --exclude=wp-super-cache,wordfence --quiet
# Specific plugin to a pinned version
wp plugin update yoast-seo --version=22.4 --quiet
# Themes
wp theme update --all --quiet
# Cleanup
wp cache flush
wp rewrite flush
wp transient delete --all
# Smoke-test the homepage post-update
curl -fsS -o /dev/null -w "%{http_code}\n" https://example.com/ | grep -q '^200$' \
|| { echo "homepage non-200 after update" >&2; exit 1; }
The smoke-test step matters: a plugin update can put the site into a fatal error state where wp itself still works but every page returns 500. Curling the homepage from inside the deploy script catches that before traffic gets routed back.
4. Multisite deploys without forgetting a subsite
In a multisite network, every command needs the right --url flag — and "I'll run it on the main site, it should propagate" is the classic source of half-migrated networks. Pattern that holds:
#!/usr/bin/env bash
set -euo pipefail
# Network-wide plugin install (NOT per-subsite)
wp plugin install advanced-custom-fields-pro --activate-network
# Per-subsite operations — iterate explicitly
mapfile -t URLS < <(wp site list --field=url)
for url in "${URLS[@]}"; do
echo "::group::$url"
wp --url="$url" plugin update --all --quiet
wp --url="$url" cache flush
wp --url="$url" rewrite flush
echo "::endgroup::"
done
--activate-network is the multisite-aware activation flag — it activates the plugin for every subsite AND prevents per-subsite activation. Forgetting it on a security-related plugin (Wordfence, iThemes Security) is the kind of mistake that takes a postmortem to find.
The mapfile pattern captures all URLs into a Bash array before the loop runs — so if one subsite's update kills the wp-cron daemon mid-loop, you still have the list of remaining subsites to retry.
5. Cache and OPcache flush as the last deploy step
A WordPress deploy that doesn't flush all four cache layers ends up serving stale content from somewhere. The four layers, in flush order:
# 1. Object cache (Redis/Memcached) — invalidates wp_options, transients
wp cache flush
# 2. Page cache (whichever plugin is installed)
wp super-cache flush 2>/dev/null || true # wp-super-cache
wp cache-enabler clear 2>/dev/null || true # cache-enabler
wp rocket clean --confirm 2>/dev/null || true # wp-rocket
wp w3-total-cache flush all 2>/dev/null || true # w3-total-cache
# 3. Permalink rewrite rules (always last among DB caches)
wp rewrite flush --hard
# 4. PHP OPcache — files on disk changed but OPcache still has the old bytecode
wp eval 'if (function_exists("opcache_reset")) opcache_reset();'
# Or, if PHP-FPM:
sudo systemctl reload php8.2-fpm
# 5. CDN / edge cache (Cloudflare, BunnyCDN, etc.) — outside WP, fire via curl/API
The 2>/dev/null || true pattern lets the script run on any host without knowing in advance which cache plugin is active — it tries each and ignores the ones that aren't installed. That's slightly less explicit than checking with wp plugin is-installed first, but the script stays portable across staging and production when they happen to run different cache plugins.
For an atomic deploy that uses zero-downtime deployments, OPcache flush is the most important step — without it the new release directory is live (the symlink swapped) but PHP-FPM is still executing the old bytecode it cached against the previous release path. Reload php-fpm or call opcache_reset() from the post-deploy hook.
6. Composer-managed WordPress sites
For sites where Composer is the source of truth (Roots/Bedrock, custom setups), WP-CLI's role narrows to runtime tasks — DB ops, search-replace, cron, cache flush — while plugin/theme installs flow through composer install. The deploy script looks like:
#!/usr/bin/env bash
set -euo pipefail
# Pull code + install vendor/ on the BUILD host (not production)
composer install --no-dev --prefer-dist --optimize-autoloader --classmap-authoritative
# On the production host, after symlink swap:
wp core update-db --quiet # safe if no DB changes
wp cache flush
wp rewrite flush --hard
wp eval 'opcache_reset();'
See the Composer cheatsheet for the production install flags and the composer.lock discipline that makes this work.
Common errors and fixes
| Error / symptom | Cause | Fix |
|---|---|---|
Error: This does not seem to be a WordPress installation |
Running wp outside the WP root |
cd to WP root or use --path=/var/www/current |
Error establishing a database connection from WP-CLI but wp-admin works |
wp-config.php references localhost but DB is on a Unix socket |
Add --dbhost=localhost:/var/run/mysqld/mysqld.sock to config |
Error: YIKES! It looks like you're running this as root |
Default safety guard | Set WP_CLI_ALLOW_ROOT=1 in CI, or use sudo -u www-data wp ... |
Error: Couldn't connect to localhost:80 (network plugins) |
Plugin tries to call its own REST API during activation | Run with --skip-plugins=<offender> for activation, then re-enable |
search-replace runs forever on a large site |
Each row touched even if nothing matched | Add --include-columns=post_content,post_excerpt to scope |
search-replace mangles serialized data |
--precise not set |
Always add --precise for serialised data; default scan misses some edge cases |
Fatal error: Allowed memory size exhausted |
PHP CLI memory limit too low | WP_CLI_PHP_ARGS="-d memory_limit=512M" wp ... |
wp cron event run never fires events |
DISABLE_WP_CRON true but no system cron set |
Add wp cron event run --due-now to system crontab (see Cron cheatsheet) |
Multisite: wp option get returns site #1's value |
Missing --url flag — defaults to main site |
Add --url=https://blog.example.com/ |
Plugin update failed: download failed |
Outbound HTTPS blocked or DNS broken | Test curl -fsS https://downloads.wordpress.org/; whitelist on firewall |
| Site shows old content after deploy | OPcache not flushed | wp eval 'opcache_reset();' or systemctl reload php-fpm |
wp command not found in cron |
Cron's PATH is minimal |
Use full path /usr/local/bin/wp in cron lines |
Companion: full DeployHQ deploy workflow
A WordPress deploy is the same atomic-release pattern as any PHP app, with one extra concern: the database typically lives outside the release directory, so schema changes (plugin updates, core upgrades) are durable — you can't roll them back by swapping the symlink. That's what makes the pre-deploy wp db export (workflow 2) the load-bearing step in this whole sheet.
For the end-to-end pattern — GitHub push → CI build (composer install) → DeployHQ webhook → SSH deploy hooks (the wp-cli steps above) → smoke test → symlink swap — wire it through DeployHQ's build pipelines and the deploy from GitHub guide. If a release does ship a bad plugin update, one-click rollback reverts the file tree while you wp db import the pre-deploy backup.
Start a free DeployHQ trial to wire wp-cli into a production deploy pipeline in minutes.
Related cheatsheets
- Composer cheatsheet — for the
composer install --no-devstep that ships your plugins and themes. - Cron and Crontab cheatsheet — for replacing WordPress's traffic-driven
wp-cronwith a real system cron. - Bash cheatsheet — for the
set -euo pipefailscripts that orchestrate the WP-CLI steps above. - SSH cheatsheet — for the deploy keys and
~/.ssh/configpatterns the deploy script runs over. - Docker cheatsheet — for the container patterns when WP-CLI runs inside a containerised PHP stack.
- Cheatsheets hub — every DeployHQ cheatsheet in one place.
Need help? Email support@deployhq.com or follow @deployhq on X.