How Wrong File Ownership Silently Broke Auto-Updates, Plugin Installs, and Uploads After a Server Migration
· 8 min read
The site looked perfect after the migration. DNS had propagated, SSL was live, pages loaded fast, WooCommerce checkout worked. The client confirmed everything was fine and I closed the migration ticket.
Three weeks later, three separate support requests landed in the same morning.
First: "WordPress says there's a security update available but the update button does nothing." Second: "I tried to install a new plugin and got an error about creating a directory." Third: "Image uploads are failing with an HTTP error."
Three different symptoms. One root cause. And I should have caught it during the migration.
The Symptoms Looked Unrelated
The site had been migrated from a shared cPanel server to a CloudPanel VPS running Nginx, PHP 8.2-FPM, and MariaDB 10.11. I'd used rsync over SSH to transfer the files:
rsync -avz -e ssh user@old-server:/home/account/public_html/ /home/clientsite/htdocs/
Standard migration. Database exported with mysqldump, imported on the new server, search-replace with WP-CLI to update the domain, tested the site, confirmed it loaded. Everything checked out.
But three weeks later, WordPress auto-updates had silently stopped. The site was still on WordPress 6.7.2 while 6.7.4 (a security release) had been available for two weeks. No error in the dashboard, no email notification about a failed update. Just... nothing.
Checking Auto-Update Status
I SSH'd into the new server and checked what WordPress thought about its own update capability:
wp core check-update --path=/home/clientsite/htdocs/
+---------+-------------+-------+
| version | update_type | package_url |
+---------+-------------+-------+
| 6.7.4 | minor | https://downloads.wordpress.org/... |
+---------+-------------+-------+
WordPress knew the update existed. It just couldn't apply it. I checked the update log:
wp option get auto_updater.lock --path=/home/clientsite/htdocs/
No stale lock. I tried triggering the update manually:
wp core update --path=/home/clientsite/htdocs/
Error: Could not create directory '/home/clientsite/htdocs/wp-admin/includes/'.
There it was. WordPress couldn't write to its own directories.
The Plugin Install Failure
The client's plugin install error was the same root cause wearing a different hat. When WordPress tries to install a plugin, it needs to create directories inside wp-content/plugins/. From the dashboard, the error was:
Could not create directory. /home/clientsite/htdocs/wp-content/plugins/new-plugin/
And the upload failure? Same story. WordPress writes uploaded files to wp-content/uploads/2026/07/. If it can't create the monthly directory or write files into it, uploads fail with the generic "HTTP error" that tells you nothing useful.
Finding the Actual Problem
I checked file ownership on the WordPress directory:
ls -la /home/clientsite/htdocs/
drwxr-xr-x 5 clientsite clientsite 4096 Jun 10 14:22 .
-rw-r--r-- 1 clientsite clientsite 405 Jun 10 14:22 index.php
-rw-r--r-- 1 clientsite clientsite 2764 Jun 10 14:22 wp-config.php
drwxr-xr-x 9 clientsite clientsite 4096 Jun 10 14:22 wp-content
drwxr-xr-x 5 clientsite clientsite 4096 Jun 10 14:22 wp-admin
...
Every file and directory was owned by clientsite:clientsite. That was the SSH user I'd used for the rsync transfer. On the old cPanel server, this user owned the files and PHP ran as the same user via suPHP. It worked.
But on CloudPanel, PHP-FPM doesn't run as clientsite. I checked the FPM pool configuration:
grep -E "^(user|group)" /etc/php/8.2/fpm/pool.d/clientsite.conf
user = www-data
group = www-data
PHP-FPM was running as www-data. The files were owned by clientsite. When PHP tried to write to any directory — for updates, plugin installs, or uploads — it was denied. The r-x permissions on directories meant www-data could read and traverse, but not write.
The site loaded perfectly because reading files requires only read permission. Every PHP file could be parsed and executed. Every image could be served. But the moment WordPress needed to write — to update itself, install a plugin, or save an upload — it hit a wall.
Why There Were No Errors
This is the part that catches people out. WordPress does send failure emails when an auto-update is attempted and fails — but when the filesystem check determines it can't write at all, the update is never attempted in the first place. No attempt means no failure, no failure means no email. The update mechanism quietly skips the run. No dashboard warning either, and no entry in debug.log unless you've got WP_DEBUG and WP_DEBUG_LOG enabled — which most production sites don't.
The only visible sign was the update notice sitting there, day after day, never clearing itself. If the client hadn't tried to install a plugin manually, this could have gone unnoticed for months. The site would have fallen further behind on security patches with nobody aware.
The Fix
Once you know the FPM pool user, the fix is a single command:
chown -R www-data:www-data /home/clientsite/htdocs/
Then verify the permissions are correct — 644 for files, 755 for directories:
find /home/clientsite/htdocs/ -type f -exec chmod 644 {} \;
find /home/clientsite/htdocs/ -type d -exec chmod 755 {} \;
After running these, I tested all three operations:
wp core update --path=/home/clientsite/htdocs/
Updating to version 6.7.4...
Unpacking the update...
Success: WordPress updated successfully.
Plugin install and image upload both worked immediately from the dashboard.
How to Check the Right User Before You Transfer
The mistake was assuming the rsync transfer user and the PHP-FPM user were the same. On hosting panels like CloudPanel, CyberPanel, and HestiaCP, the FPM pool user varies. Some run as the site-specific user, others as www-data, and some as a dedicated php-fpm user.
Before transferring files to a new server, check what user PHP-FPM is actually running as:
ps aux | grep '[p]hp-fpm: pool' | awk '{print $1}' | sort -u
Or check the pool config directly:
grep -r "^user" /etc/php/*/fpm/pool.d/
Then use rsync's --chown flag to set ownership during the transfer instead of fixing it after. Note that --chown requires the receiving rsync to run with root privileges, so use --rsync-path to elevate on the destination:
rsync -avz --chown=www-data:www-data --rsync-path="sudo rsync" -e ssh user@old-server:/home/account/public_html/ /home/clientsite/htdocs/
If your destination user doesn't have passwordless sudo for rsync, transfer first and then chown -R after — still faster than debugging it three weeks later.
The Post-Migration Ownership Check I Now Run on Every Site
After this incident, I added a file ownership verification step to every migration. The read-only checks are useful but not sufficient on their own:
wp core verify-checksums --path=/home/clientsite/htdocs/
wp plugin list --path=/home/clientsite/htdocs/ --format=csv | head -5
The first command verifies core file integrity — if it throws permission errors when run as the FPM user, ownership is wrong. The plugin list confirms WP-CLI can read plugin directories. But these are read-only checks. They'll pass even when the FPM user can't write a single byte.
The actual test that matters is a direct write test, run as the PHP-FPM user:
sudo -u www-data touch /home/clientsite/htdocs/wp-content/uploads/.ownership-test && \
echo "Write OK" && \
sudo -u www-data rm /home/clientsite/htdocs/wp-content/uploads/.ownership-test || \
echo "WRITE FAILED — check ownership"
If www-data (or whatever your FPM pool user is) can't create a file in the uploads directory, you've got the same problem. Run the cleanup as the same user — otherwise the rm can fail for different permission reasons and give a misleading result.
Why This Matters More Than It Looks
A site with wrong file ownership still works for visitors. Pages load, products display, checkouts complete. The damage is invisible: security updates don't apply, plugins can't be installed or updated, and uploaded content fails. Over weeks and months, the site drifts further behind on patches while appearing healthy.
I manage 70+ WordPress sites across multiple servers. After this incident, I built ownership verification into my post-migration runbook. A one-line chown during the transfer prevents a support ticket three weeks later — and more importantly, prevents a site sitting unpatched because auto-updates silently gave up.
Stop Firefighting. Start Maintaining.
I manage 70+ WordPress sites for agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.
