Update Every WordPress Site on Your Server in One Pass
If you're logging into wp-admin on twenty different cPanel accounts every Monday morning to click "Update All," you're spending hours on something a fifteen-line script can do in minutes. Here's how to update WordPress core, plugins, and themes across every site on a WHM reseller account or multi-tenant VPS without breaking anything.
Symptom: Updates Don't Scale Past a Handful of Sites
One WordPress site, wp-admin is fine. Five sites, it's tedious. Twenty or more sites on one server — each with its own cPanel account, its own plugin list, its own PHP version — and manual updates become a part-time job. Agencies and resellers running Getwebup VPS or WHM plans hit this wall fast: you either fall behind on updates (bad, because most WordPress breaches come through outdated plugins) or you burn a full day every week clicking through the same screens on every account.
The fix isn't a plugin that promises to "manage all your sites" from one dashboard — most of those cost more than the time they save and still need per-site babysitting. It's a WP-CLI loop that runs server-side, where you already have root.
Cause: WP-CLI Is Per-Site, Not Per-Server, By Default
WP-CLI is built to operate on one WordPress install at a time — you cd into a directory and run a command. On shared cPanel hosting each account is jailed, so there's no built-in way to fan a command out across accounts. On a VPS or WHM reseller box where you hold root, though, you can script around that jail by looping over every cPanel user and running WP-CLI as that user, one at a time.
The trick is finding each site's real document root reliably — addon domains, subdomains, and multisite installs don't all live in ~/public_html, and guessing wrong either updates the wrong thing or updates nothing silently.
Fix: A Safe, Server-Wide Update Script
Step 1 — List Every cPanel Account and Its Real Docroot
Don't assume public_html. cPanel stores the actual document root for every domain (main, addon, and subdomain) in each account's userdata file. As root:
for user in $(ls /var/cpanel/users); do
docroot=$(grep -m1 "^documentroot:" /var/cpanel/userdata/$user/main | awk '{print $2}')
echo "$user -> $docroot"
done
Run that first and just look at the output. If a docroot looks wrong (empty, or pointing at a parked domain with no WordPress install), fix that account manually before it goes anywhere near a loop.
Step 2 — Confirm It's Actually WordPress Before Touching It
Not every account on the server runs WordPress — some might be static sites, Laravel apps, or plain HTML. Check for wp-config.php before running anything:
if su - "$user" -c "test -f $docroot/wp-config.php"; then
echo "$user is WordPress"
fi
Step 3 — Snapshot Before You Update Anything
This is the step people skip and regret. If you're on a Getwebup VPS, take a snapshot before a bulk run — one command, and you have a full rollback point if a plugin update goes sideways on a dozen sites at once:
# From your VPS control panel, or via the API if you've scripted deployments
# Snapshot first. Always. A bulk update is exactly the scenario snapshots exist for.
On shared/WHM reseller plans without snapshot access, at minimum run a quick per-account backup through cPanel's Backup Wizard, or JetBackup if it's enabled, before the loop runs.
Step 4 — Dry Run First
Before you update anything for real, list what's outdated everywhere, so you know the blast radius:
for user in $(ls /var/cpanel/users); do
docroot=$(grep -m1 "^documentroot:" /var/cpanel/userdata/$user/main | awk '{print $2}')
if su - "$user" -c "test -f $docroot/wp-config.php" 2>/dev/null; then
echo "=== $user ($docroot) ==="
su - "$user" -c "wp --path=$docroot plugin list --update=available --format=table" 2>/dev/null
fi
done
Skim this output for anything that looks like a major version jump (say, WooCommerce 8 to 9, or a page builder's major release) — those are the ones you update on a staging copy first, not in the bulk run.
Step 5 — Run the Real Update, One Site at a Time
Update core, plugins, and themes, but keep it sequential rather than parallel. Parallel updates across a dozen sites spike CPU and I/O at the same moment, which is how a bulk update turns into a server-wide slowdown that panics every customer at once:
for user in $(ls /var/cpanel/users); do
docroot=$(grep -m1 "^documentroot:" /var/cpanel/userdata/$user/main | awk '{print $2}')
if su - "$user" -c "test -f $docroot/wp-config.php" 2>/dev/null; then
echo "Updating $user..."
su - "$user" -c "wp --path=$docroot core update" 2>&1 | tee -a /root/wp-bulk-update.log
su - "$user" -c "wp --path=$docroot plugin update --all" 2>&1 | tee -a /root/wp-bulk-update.log
su - "$user" -c "wp --path=$docroot theme update --all" 2>&1 | tee -a /root/wp-bulk-update.log
fi
done
The log file is what saves you later — when a customer emails three days from now saying their site "suddenly looks weird," you can grep the log for their username and see exactly what changed and when.
Step 6 — Exclude Sites That Shouldn't Auto-Update
Some sites — a client running a heavily customized checkout flow, or one still on an old PHP version because of a legacy plugin — need to opt out. Keep a plain text exclusion list and check against it:
SKIP_LIST="/root/wp-update-skip.txt"
for user in $(ls /var/cpanel/users); do
grep -qx "$user" "$SKIP_LIST" 2>/dev/null && continue
# ...rest of the loop
done
Prevention: Automate It, But Watch It
Once the script is solid, cron it — but not blindly:
| Practice | Why it matters |
|---|---|
| Run weekly, not daily | Gives you time to catch a bad plugin update before the next run compounds it |
| Keep the log file and rotate it | You'll need it the first time a customer disputes what changed on their site |
| Snapshot or backup immediately before the cron fires | A scheduled job with no fresh backup is a scheduled disaster |
| Email yourself the log summary | A silent cron job is one nobody notices has been failing for a month |
| Exclude major version jumps from auto-update | Core and security patches are safe to automate; WooCommerce major versions are not |
# crontab -e (as root)
0 3 * * 1 /root/wp-bulk-update.sh >> /root/wp-bulk-update-cron.log 2>&1
Running it at 3 AM on a Monday keeps the CPU spike off business hours and gives you the whole week to notice and fix anything before the next run.
Common Pitfalls
- Running the loop as root without
su - user: file ownership on updated plugins ends up owned by root instead of the cPanel user, which then breaks that account's ability to update or edit files through wp-admin afterward. - Assuming every account's docroot is
public_html: addon domains and subdomains routinely live elsewhere — always read it from/var/cpanel/userdata/rather than hardcoding a path. - No per-site error isolation: if one site's database connection is down, a poorly written loop can stall on that account. Add
timeout 300in front of eachwpcall so one broken site can't hang the whole run. - Updating during business hours: a plugin update that briefly puts a site in maintenance mode is a non-issue at 3 AM and a support ticket at 2 PM.
If you'd rather not maintain this script yourself, Getwebup's managed VPS and reseller plans include scheduled WordPress maintenance as part of support — worth asking about if you're managing more than a handful of client sites on one server.
Frequently asked questions
Will this work on shared cPanel hosting, not just a VPS?
Not directly — shared hosting doesn't give you root, so you can't loop across other accounts on the same server. On shared hosting, WP-CLI still works great per-site through cPanel Terminal, but bulk updates across multiple unrelated accounts need root, which means a VPS or a WHM reseller/dedicated plan.
Is it safe to run wp core update on every site without checking first?
Core updates are generally low-risk since WordPress core is heavily backward-compatible, but plugin and theme updates are where things break. Always dry-run plugin list --update=available first and keep a skip list for sites running customizations that a plugin update could conflict with.
What if a site uses a different PHP version than the server default?
WP-CLI respects the PHP version set for that cPanel account through MultiPHP Manager, so as long as you're running the command as that user (su - user), it picks up the right PHP binary automatically. Running it as root without su can silently use the wrong PHP version.
How do I know if an update broke something across many sites at once?
This is exactly what the log file from Step 5 is for. After a run, grep it for "Error", "Fatal", or "Warning" across all accounts in one pass, so you're not waiting for customers to notice and report it first.
Can I update WordPress multisite networks the same way?
Yes, but target the network root, not each site's URL — run wp core update and wp plugin update --all from the main install's path, since multisite shares one WordPress core and plugin set across all subsites.