How to migrate WordPress by hand: files, database, DNS
This guide walks through a manual WordPress migration from one server to another: copying the files, moving the database, rewriting URLs and switching DNS with close to zero downtime. It is written for anyone comfortable with SSH and a shell. No migration plugin is required, and at the end we cover when a plugin is actually the reasonable choice.
What actually has to move
A WordPress site is three things: the wp-content directory (themes, plugins, uploads), the MySQL or MariaDB database, and a handful of settings in wp-config.php. WordPress core itself is replaceable, you can download the same version fresh on the target server. Before touching anything, check that the target has the same or a newer PHP version, the extensions your plugins need (common ones: mysqli, gd or imagick, curl, zip) and enough disk space for the uploads folder.
Step 1: copy the files
Use rsync over SSH. It preserves timestamps, shows progress and lets you re-run it later to pick up only the files that changed:
rsync -avz --exclude 'wp-content/cache' old-server:/var/www/example.com/ /var/www/example.com/
Exclude cache directories, they are regenerated anyway and often contain hardcoded paths. If the old host only gives you FTP, download one tar archive instead of thousands of individual files:
tar czf site.tar.gz -C /var/www/example.com .
Step 2: dump and import the database
On the old server:
mysqldump --single-transaction --default-character-set=utf8mb4 -u dbuser -p dbname > site.sql
--single-transaction gives a consistent snapshot without locking the live site. Copy the file over and import it on the new server:
mysql -u dbuser -p newdbname < site.sql
If the database name or user changes on the new server, create them and grant privileges first. Keep the character set utf8mb4 on both sides: a silent downgrade to utf8 truncates emoji and any other 4-byte characters.
Step 3: rewrite the URL the safe way
If the domain stays the same, skip this step. If it changes, never run a plain SQL UPDATE ... REPLACE() across the tables. WordPress stores widget settings, theme options and plugin data as serialized PHP arrays, and those arrays record string lengths. A blind replace changes the strings but not the lengths, and the data silently fails to unserialize: widgets vanish, theme options reset.
The correct tool is wp-cli, which is serialization aware:
wp search-replace 'https://old-domain.com' 'https://new-domain.com' --all-tables --dry-run wp search-replace 'https://old-domain.com' 'https://new-domain.com' --all-tables
Run the dry run first and read the per-table report. If wp-cli is not available, use a serialization-safe script such as Search Replace DB, then delete it from the server the moment you are done, otherwise it is a publicly reachable backdoor.
Step 4: edit wp-config.php
Update the database block to match the new server:
define( 'DB_NAME', 'newdbname' ); define( 'DB_USER', 'newdbuser' ); define( 'DB_PASSWORD', 'newpassword' ); define( 'DB_HOST', 'localhost' );
Check three more things. First, $table_prefix must match what the dump actually contains. Second, keep the existing authentication keys and salts, changing them logs every user out. Third, if the old config defined WP_HOME and WP_SITEURL, update them to the new domain, because they override the values stored in the database.
Step 5: test through the hosts file, then switch DNS
Lower the DNS TTL on the A record to 300 seconds at least a day before the move, so the old value expires quickly once you switch. While DNS still points at the old server, test the new one by forcing your own machine to resolve the domain there. Add a line to /etc/hosts (on Windows: C:\Windows\System32\drivers\etc\hosts):
203.0.113.10 example.com www.example.com
Now your browser hits the new server while everyone else still sees the old one. Test the front page, the wp-admin login, permalinks on a few deep pages, image loading, and any form or checkout. When everything works, update the A and AAAA records, remove the hosts line, and keep the old server running for at least 48 hours: some resolvers ignore TTL, and you want the old access log to go quiet before you shut it down.
Common failure points and when a plugin is fine
Mixed content, permalinks, permissions
- Mixed content. If the site moved from http to https during the migration, hardcoded
http://URLs in content and options keep loading over plain http and the browser blocks them. Fix it with one more search-replace fromhttp://example.comtohttps://example.com. - Permalinks return 404. The front page works but every subpage is a 404. That is missing rewrite configuration: on Apache the
.htaccessfile was not copied ormod_rewriteis off; on nginx the server block lackstry_files $uri $uri/ /index.php?$args;. Re-saving the permalink settings in wp-admin regenerates.htaccess. - File permissions. If uploads fail or plugins cannot update, the files probably belong to the wrong user after the copy. Set ownership to the PHP worker user and permissions to 755 for directories, 644 for files:
chown -R www-data:www-data /var/www/example.com find /var/www/example.com -type d -exec chmod 755 {} + find /var/www/example.com -type f -exec chmod 644 {} +
Plugin migrator or manual: which one when
Duplicator, All-in-One WP Migration and similar plugins are fine for a small site: a gigabyte or two at most, one person editing, and a tolerance for repeating the whole export if something fails. They package everything into one archive and handle the URL rewrite for you.
Manual migration is safer when the uploads folder is large (plugin packaging dies on PHP memory and execution limits), when the site is busy and you need a final incremental rsync plus a fresh dump right before the DNS switch, for multisite installs, and whenever you want to know exactly what moved. The manual path is also the only one that gives you a real rehearsal: the hosts-file test against the live database is something an archive-based plugin cannot offer.
If you are moving to us, both WordPress hosting and plain web hosting come with SSH and wp-cli available, so every command in this guide works as written. Support can also handle the migration for you if you would rather hand it over.