If your web server suddenly throws 403 errors, cron jobs silently stop running, or MySQL refuses to start right after a server migration, the culprit is rarely corrupted data — it is almost always wrong file ownership caused by UID/GID mismatches between the old and new server. This guide shows you exactly how to diagnose the problem and fix it permanently, with real commands you can run right now.
Why File Ownership Breaks During Migration
Every Linux user and group is stored internally as a numeric ID (UID/GID), not a name. When you run rsync -a to copy a website or database directory to a new server, rsync tries to preserve ownership — but it can only do that correctly if the numeric IDs line up on both machines.
In practice they almost never do. On your old server, www-data might be UID 33. On a freshly provisioned server, the first service you installed may have claimed UID 33 for something else entirely, pushing www-data to UID 1001. Rsync copies the numeric owner as-is, so files that were readable by the web server on the old box are now silently owned by the wrong account on the new one.
This is especially common when:
- You provisioned the new server with a different OS image or a newer distro version that creates system accounts in a different order
- The old server had custom users created manually over the years, in a sequence that's impossible to replicate exactly
- You migrated using an imaging or snapshot tool that doesn't preserve the
/etc/passwdand/etc/groupmapping - Multiple services (Docker, mail, database) were installed in a different order, so system UIDs shifted
The symptoms rarely show up during the copy itself — rsync will report success with zero errors. They show up minutes or hours later when a service tries to actually use those files:
- Nginx or Apache returns
403 Forbiddeneven though the files clearly exist - PHP-FPM logs
Permission deniedwhen writing to cache or upload directories - MySQL/MariaDB fails to start because
/var/lib/mysqlis not owned by themysqluser - Cron jobs stop firing because the script directory isn't writable by the cron user
- Mail delivery breaks because
vmailordovecotcan't access the mailbox files - Deploy scripts (Git pulls, CI/CD pipelines) fail silently because the deploy user lost write access
💡 None of these worked? Skip the guesswork.
Get Expert Help →Diagnosing UID/GID Mismatches Fast
Before fixing anything, confirm this is actually the problem. It takes two minutes and saves hours of guessing at firewall rules or SELinux contexts that aren't actually the cause.
Run this on the old server and the new server for every service account you use (web server user, database user, mail user):
id www-data
id mysql
id vmail
If the numbers differ between servers, you have found the root cause. Write both sets of numbers down before you do anything else — you'll need them for the fix.
ls -lan /var/www/yoursite | head -20
stat -c '%U %G %u %g' /var/www/yoursite/index.php
The %u and %g columns show the raw numeric owner. If it doesn't match the UID that id www-data returned on the new server, that file is orphaned — owned by a number, not a recognizable user, even if ls -l displays a username that looks correct.
find /var/www -nouser -o -nogroup
This lists every file whose numeric owner or group doesn't resolve to any account on the current system — a fast way to size up how widespread the problem is before you fix it. Run it against / if you suspect the issue extends beyond the web root:
find / -xdev -nouser -o -nogroup 2>/dev/null
tail -n 50 /var/log/nginx/error.log
journalctl -u mysql -n 50 --no-pager
Nginx will usually log something like 13: Permission denied which confirms this is an ownership/permissions issue, not a config or firewall problem. MySQL will typically log Can't create/write to file or refuse to initialize the data directory at all.
After remapping, re-run the orphan check to confirm nothing was missed, then restart the relevant services one at a time so you can isolate any remaining issue:
find /var/www -nouser -o -nogroup
systemctl restart nginx php8.2-fpm mysql
Load the site in a browser and tail the logs simultaneously — if 403s persist after ownership looks correct, check directory execute bits (chmod 755 on directories) since rsync sometimes preserves overly restrictive modes from the source server as well.
If you'd rather not troubleshoot UID drift manually every time you provision a new server, our team handles the entire cutover — including account alignment, ownership verification, and zero-downtime DNS switchover — as part of our server migration service.
A Real-World Example Walkthrough
Consider a typical WordPress migration from an older Ubuntu 20.04 box to a fresh Ubuntu 24.04 server. On the old server, www-data is UID 33 because it was one of the first packages installed years ago. On the new server, Docker was installed first and claimed a system UID range that pushed www-data to UID 998 during provisioning.
After running a straightforward rsync -a without --numeric-ids, the WordPress wp-content/uploads directory ends up owned by UID 33 on the new box — which now belongs to a completely different, unrelated system account. WordPress can't write new media uploads, plugin updates fail with permission errors, and the site owner assumes the migration itself corrupted their install.
The fix takes under five minutes once diagnosed: confirm the UID mismatch with id www-data on both servers, then run find /var/www -uid 33 -exec chown www-data {} \; followed by the equivalent for the group, and restart PHP-FPM. No data was ever lost — it was purely an ownership label problem.
Preventing This on Every Future Migration
Once you've fixed the immediate issue, build these steps into your migration checklist so it never bites you again:
- Always run
idfor every service account on both servers before any data moves - Provision service accounts with matching UID/GID on the new server as part of initial setup, not as an afterthought
- Always transfer with
rsync -avz --numeric-ids --acls --xattrsto preserve exact ownership and permissions - Run
find / -nouser -o -nogroupas a post-migration health check, not just when something breaks - Document the UID/GID map for every environment so future migrations start from a known baseline
- Test file writes (uploads, cache generation, log rotation) immediately after cutover, before you point DNS at the new server
When to Bring in Migration Specialists
UID/GID drift is one of dozens of small but critical details — DNS TTL timing, database replication lag, SSL certificate reissuance, hardcoded IPs in application configs — that turn a "simple" server move into hours of downtime when missed. If your migration involves production databases, e-commerce checkout flows, or compliance-sensitive data, it's worth having a team that has done this hundreds of times handle the cutover so ownership, permissions, and DNS all land correctly on the first attempt.
Frequently Asked Questions
Conclusion
Wrong file ownership after a server migration is almost always a UID/GID mismatch, not data corruption — and it's fixable in minutes once you know where to look. Align your service account IDs before migrating, always use --numeric-ids with rsync, and run an orphan-file check as a standard post-migration step. Get this right once and you'll never chase a mystery 403 error or a MySQL startup failure after a server move again.