Moving a WordPress site from one host to another is one of those tasks that sounds simple but has enough moving parts to go wrong in subtle ways. A WordPress site is not a single thing you can copy – it is a PHP application, a MySQL database, a collection of uploaded files, and a set of configuration values that tie everything together. All of these need to move, and they need to be reconfigured to work at the destination.
This guide covers the full manual migration process step by step, explains where things commonly break, and discusses how modern migration tools eliminate most of the manual work.
What a WordPress site consists of
Before migrating, it helps to understand what you are actually moving. A WordPress installation has four components that need to travel together:
WordPress core files. The PHP files that make up WordPress itself –
wp-admin/
,
wp-includes/
, and the root files like
wp-login.php
,
wp-cron.php
, and
index.php
. These are identical across all WordPress installations of the same version, so they can be reinstalled fresh at the destination rather than copied. But if you have made any modifications to core files (which you should not have, but some developers do), those changes would be lost with a fresh install.
wp-content directory. This is where your site’s unique content lives:
-
wp-content/themes/– your active theme and any installed themes -
wp-content/plugins/– all installed plugins, active or not -
wp-content/uploads/– every image, PDF, video, and file you have ever uploaded through the media library, organized in year/month subdirectories -
wp-content/mu-plugins/– must-use plugins, if any - Custom directories created by plugins (like cache directories, backup storage, etc.)
The database. WordPress stores all your content in a MySQL (or MariaDB) database – posts, pages, comments, user accounts, plugin settings, theme customizer values, widget configurations, menu structures, and site options. The database also stores your site URL in multiple places, which is why a simple database copy without URL replacement does not work.
wp-config.php. The configuration file that connects everything. It contains database credentials, authentication keys and salts, the table prefix, debug settings, and any custom constants your setup requires. This file needs to be recreated or modified for the destination environment because the database credentials will be different.
Pre-migration checklist
Skipping preparation is the most common cause of failed migrations. Work through this list before touching any files.
Document your current setup
Record these details from your current host:
- PHP version. Check via your hosting control panel or create a
phpinfo()file. Your new host needs to support the same PHP version (or newer, if your plugins are compatible). Switching from PHP 8.1 to 7.4 during migration will break things. - MySQL/MariaDB version. Some plugins use features specific to MySQL 5.7+ or 8.0+. Note the version.
- WordPress version. Run
wp core versionvia WP-CLI if available, or check thewp-includes/version.phpfile. - Active plugins. Note every active plugin and its version. Some plugins store data outside the standard WordPress tables and may need special handling.
- Custom configurations. Check
wp-config.phpfor any custom constants beyond the defaults – custom upload paths, custom content directories, multisite configuration, or plugin-specific constants.
Create a full backup
Before making any changes, create a complete backup of both files and database on the source host. This is your safety net if anything goes wrong.
# Backup all WordPress files
cd /path/to/wordpress
tar -czf ~/wordpress-backup.tar.gz .
# Backup the database
wp db export ~/database-backup.sql
If you do not have WP-CLI, use
mysqldump
:
mysqldump -u db_username -p db_name > ~/database-backup.sql
Check disk space at the destination
Verify that your new hosting account has enough storage for your files and database. Check your current usage:
# Total file size
du -sh /path/to/wordpress/
# Database size
wp db size --tables
The destination needs at least this much space, plus headroom for the database import process (which temporarily requires space for both the SQL file and the imported data).
Step 1: Export the database
The database export is the most critical step because it contains all your content.
Using WP-CLI
WP-CLI is the cleanest way to export:
wp db export wordpress-db.sql --add-drop-table
The
--add-drop-table
flag adds
DROP TABLE IF EXISTS
statements before each
CREATE TABLE
, which ensures a clean import at the destination if there are any existing tables.
Using mysqldump
If WP-CLI is not available:
mysqldump -u db_username -p \
--single-transaction \
--routines \
--triggers \
--add-drop-table \
db_name > wordpress-db.sql
--single-transaction
ensures a consistent snapshot without locking the database, so your live site continues working during the export. This is important if your site has traffic during the migration.
Using phpMyAdmin
If you only have cPanel access without SSH:
- Open phpMyAdmin from cPanel
- Select your WordPress database
- Click Export → Custom (not Quick)
- Check “Add DROP TABLE” under Object creation options
- Set compression to gzip for large databases
- Click Go
Large database considerations
Databases over 500 MB need extra attention:
- Export compression. Pipe the dump through gzip to reduce transfer time:
mysqldump -u db_username -p db_name | gzip > wordpress-db.sql.gz - Timeout issues. Large exports can hit PHP timeout limits in phpMyAdmin. Always prefer command-line tools for large databases.
- Binary data. If your database contains serialized PHP data with binary content (some plugins store this), ensure the export uses
--hex-blobto preserve binary data correctly.
Step 2: Transfer files
You need to move the entire WordPress directory to the new host. The method depends on what access you have.
Using SSH (rsync)
If you have SSH access to both the source and destination, rsync is the best tool. It compresses data during transfer, handles interruptions gracefully (you can resume), and preserves file permissions:
# From the source server, push to destination
rsync -avz --progress \
/path/to/wordpress/ \
user@new-host:/path/to/destination/
# Or from the destination, pull from source
rsync -avz --progress \
user@old-host:/path/to/wordpress/ \
/path/to/destination/
The trailing slash on the source path matters – it means “copy the contents of this directory,” not “copy the directory itself.”
Using SSH (SCP)
If rsync is not available but SSH is:
# Create a compressed archive first
tar -czf wordpress-files.tar.gz -C /path/to/wordpress .
# Transfer the archive
scp wordpress-files.tar.gz user@new-host:/path/to/destination/
# Extract at the destination
ssh user@new-host "cd /path/to/destination && tar -xzf wordpress-files.tar.gz"
Using SFTP
If you only have SFTP access (common on shared hosting), use an SFTP client like FileZilla:
- Connect to the source host, navigate to the WordPress directory
- Download everything to your local machine
- Connect to the destination host
- Upload everything to the new document root
This is the slowest method because everything passes through your local machine. For sites with thousands of media files, this can take hours. If possible, compress the files on the source server first, download the single archive, upload it to the destination, and extract it there.
What to exclude
Some files and directories should not be transferred:
- Cache directories (
wp-content/cache/,wp-content/w3tc-config/, etc.) – these will be regenerated - Backup archives stored by plugins (UpdraftPlus, All-in-One WP Migration backups can be gigabytes)
- Debug logs (
wp-content/debug.log) - Server-specific files (
.htaccessmay need rewriting for the new server,php.inior.user.inimay have different requirements)
Do transfer
wp-content/uploads/
in its entirety. Missing media files are the most visible migration failure.
Step 3: Create the database at the destination
On your new host, create an empty MySQL database and a user with full privileges:
CREATE DATABASE wordpress_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'strong_password_here';
GRANT ALL PRIVILEGES ON wordpress_db.* TO 'wp_user'@'localhost';
FLUSH PRIVILEGES;
Use
utf8mb4
– it is the correct character set for WordPress since version 4.2. Using
utf8
(which is actually
utf8mb3
in MySQL) will cause issues with emoji and some non-Latin characters.
Most hosting control panels provide a MySQL database wizard that handles this. Use it if available – it ensures the user and database are created with the correct privileges for your hosting environment.
Step 4: Import the database
Using WP-CLI
wp db import wordpress-db.sql
Using the mysql command
mysql -u wp_user -p wordpress_db < wordpress-db.sql
If the SQL file is gzipped:
gunzip < wordpress-db.sql.gz | mysql -u wp_user -p wordpress_db
Using phpMyAdmin
- Open phpMyAdmin, select the new database
- Click Import
- Choose the SQL file (or gzipped SQL)
- Click Go
phpMyAdmin has upload size limits (usually 50 MB or 128 MB depending on the server’s PHP configuration). For larger databases, use the command line or ask your host to increase the limit.
Import failures
Common import failures and their causes:
- “MySQL server has gone away” during import – the SQL file contains a statement larger than
max_allowed_packet. Ask your host to increase this value, or addSET GLOBAL max_allowed_packet=268435456;before importing. For a detailed walkthrough of this error, see MySQL server has gone away: what it means and how to fix it. - Character set errors – the source database used a character set that the destination MySQL does not support. This is rare with modern MySQL versions but can happen when migrating from very old servers.
- Foreign key constraint failures – some plugins create tables with foreign key relationships. The import order may violate these constraints. Wrapping the import with
SET FOREIGN_KEY_CHECKS=0;at the beginning andSET FOREIGN_KEY_CHECKS=1;at the end resolves this.
Step 5: Update wp-config.php
The
wp-config.php
file needs to reflect the new hosting environment. The critical values to update:
// Database credentials - must match what you created in step 3
define('DB_NAME', 'wordpress_db');
define('DB_USER', 'wp_user');
define('DB_PASSWORD', 'strong_password_here');
define('DB_HOST', 'localhost');
// Table prefix - must match the source database
$table_prefix = 'wp_';
Keep the authentication keys and salts from your original
wp-config.php
. These are used to validate cookies and session tokens. If you change them, all logged-in users will be logged out (which may be acceptable for a migration, but it is worth knowing).
If the new host uses a different MySQL host than
localhost
(some hosts use a separate database server with a hostname like
db.hostingprovider.com
), update
DB_HOST
accordingly.
Step 6: Search and replace URLs
This is the step that most manual migrations get wrong. WordPress stores the full site URL in the database – not just in the
siteurl
and
home
options, but throughout post content, widget settings, theme customizer values, and serialized plugin data.
A naive
UPDATE wp_options SET option_value = 'https://new-domain.com' WHERE option_name = 'siteurl';
changes the site URL in the options table, but leaves hundreds or thousands of references to the old URL scattered throughout the database.
The serialized data problem
WordPress plugins store settings as serialized PHP arrays in the database. A serialized string looks like this:
s:49:"https://old-domain.com/wp-content/uploads/logo.png";
The
s:49
means “string of 49 characters.” If you do a SQL REPLACE to change the domain name, the string length changes, but the length prefix does not. The serialized data becomes corrupt, and WordPress cannot unserialize it. The plugin’s settings are lost.
This is why you must use a serialization-aware search and replace tool.
Using WP-CLI
WP-CLI’s
search-replace
command handles serialized data correctly:
wp search-replace 'https://old-domain.com' 'https://new-domain.com' --all-tables
The
--all-tables
flag includes non-standard tables created by plugins. Without it, only the core WordPress tables are processed.
Run it with
--dry-run
first to see what would change:
wp search-replace 'https://old-domain.com' 'https://new-domain.com' --all-tables --dry-run
Common replacements needed
You may need multiple search-replace passes:
# HTTPS URL
wp search-replace 'https://old-domain.com' 'https://new-domain.com' --all-tables
# HTTP URL (if old site was HTTP or mixed)
wp search-replace 'http://old-domain.com' 'https://new-domain.com' --all-tables
# Without protocol (some plugins store paths without protocol)
wp search-replace '//old-domain.com' '//new-domain.com' --all-tables
# If the file path changed
wp search-replace '/home/olduser/public_html' '/home/newuser/public_html' --all-tables
Using a PHP script
If WP-CLI is not available, the Search Replace DB script by Interconnect IT handles serialized data. Upload it to your WordPress root, run it from the browser, then delete it immediately – it provides unauthenticated database access.
Step 7: Set file permissions
After transferring files, ensure the permissions are correct for the new server:
# Directories should be 755
find /path/to/wordpress -type d -exec chmod 755 {} \;
# Files should be 644
find /path/to/wordpress -type f -exec chmod 644 {} \;
# wp-config.php should be more restrictive
chmod 640 wp-config.php
The file owner should be the user that PHP runs as on the new host. On shared hosting, this is usually your account user. On a VPS, it may be
www-data
(Debian/Ubuntu) or
nginx
/
apache
(RHEL/CentOS). If the ownership is wrong, WordPress will not be able to write to the uploads directory or update plugins. See The uploaded file could not be moved to wp-content/uploads for a detailed explanation of how file permissions affect WordPress uploads.
Step 8: Test before switching DNS
This is the step that separates clean migrations from stressful ones. Before pointing your domain to the new host, verify that the site works on the new server.
Using a hosts file override
Edit your local machine’s hosts file to temporarily point your domain to the new server’s IP address:
On macOS/Linux:
/etc/hosts
On Windows:
C:\Windows\System32\drivers\etc\hosts
Add a line:
203.0.113.50 yourdomain.com www.yourdomain.com
Replace
203.0.113.50
with your new server’s IP address. Now when you visit
yourdomain.com
in your browser, your machine sends the request to the new server instead of the old one. Everyone else still sees the old server.
What to test
- Homepage loads without errors or missing styles
- Internal pages and posts display correctly with images loading
- Admin dashboard (
/wp-admin/) loads and you can log in - Media library shows images and you can upload a new test file
- Forms submit correctly (contact forms, WooCommerce checkout if applicable)
- SSL certificate is valid and HTTPS works
- Permalinks work (visit a post directly by its URL, not just through navigation)
If permalinks return 404 errors, the server’s rewrite rules need configuration. On Nginx, ensure the
try_files $uri $uri/ /index.php?$args;
directive is in the server block. On Apache, ensure
mod_rewrite
is enabled and
.htaccess
is being read (check
AllowOverride All
).
Fixing the “white screen”
If the site shows a blank white page, enable debugging temporarily:
// In wp-config.php
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', true);
Check
wp-content/debug.log
for the specific PHP error. Common causes after migration:
- Wrong PHP version – a plugin requires PHP 8.0+ but the new host runs 7.4
- Missing PHP extensions – the site needs an extension (like
imagick,intl, orsodium) that is not installed - Database connection error – wrong credentials in
wp-config.php
Step 9: Switch DNS
Once the site is verified on the new server, update your domain’s DNS records:
- Log into your domain registrar (or DNS provider)
- Update the A record to point to the new server’s IP address
- If using
www, update the CNAME or A record forwwwas well - If you have MX records (email), leave them unchanged unless you are also moving email
DNS propagation
DNS changes do not take effect instantly. Different DNS resolvers around the world cache records for the duration of the TTL (Time to Live). If your TTL was set to 86400 (24 hours), some visitors will see the old server for up to 24 hours after the change.
Preparation trick: Lower your DNS TTL to 300 (5 minutes) a day or two before the migration. This means that when you make the actual switch, most DNS caches expire within 5 minutes. After propagation is complete, raise the TTL back to a normal value (3600 or higher).
During propagation
During DNS propagation, some visitors hit the old server and some hit the new one. This is unavoidable. To prevent data loss:
- Disable comments and registrations on the old site during propagation, so no content is created on the old server that would be lost
- Put the old site in maintenance mode once you are confident the new site is working
- If running WooCommerce, consider a brief maintenance window during propagation to avoid orders hitting the old database
Step 10: Post-migration cleanup
After DNS has fully propagated and the new site is confirmed working:
- Remove the hosts file entry from your local machine
- Regenerate permalinks – go to Settings → Permalinks and click Save (even without changing anything) to regenerate rewrite rules
- Clear all caches – page cache, object cache, browser cache, CDN cache
- Test email delivery – WordPress sends emails for password resets, comment notifications, and WooCommerce order confirmations. Verify these work on the new host
- Verify cron jobs –
wp-cron.phphandles scheduled tasks (scheduled posts, plugin updates). Verify it is running - Check SSL certificate – ensure HTTPS works and the certificate covers your domain
- Delete the old site after a comfortable period (a week is reasonable) once you are certain everything works
Migration plugins vs manual migration
The manual process described above works and gives you full control. It is also time-consuming, error-prone, and requires SSH access and command-line comfort. For sites with large databases, complex plugin configurations, or WooCommerce with thousands of products, manual migration has significant risk of missing something.
Migration plugins automate most of these steps. The two main approaches are:
Push-based plugins
Plugins like All-in-One WP Migration, Duplicator, and UpdraftPlus package the site (files + database) into an archive on the source server, download it, and re-import it at the destination.
The limitation is that the entire process runs through PHP in your browser. Large sites hit PHP memory limits, execution timeouts, and upload size limits. If your browser tab closes or your connection drops during transfer, you start over. Sites over 1-2 GB often require the premium version of these plugins to handle the size, and even then, the browser-dependent transfer can be unreliable.
Pull-based migration
A more robust approach is pull-based: the destination server connects to the source and pulls data directly, server-to-server, without your browser in the middle.
This eliminates browser dependency entirely. The transfer runs between servers (often on fast data center connections), can handle interruptions and resume from where it stopped, and is not limited by PHP’s upload size or browser timeouts.
Pull-based migration can transfer the database in row-level batches (rather than one massive SQL dump), which means it works reliably even for databases with hundreds of thousands of rows. Files transfer in small chunks, so a single large file or a dropped connection does not require restarting the entire migration.
How migration works on Hostney
Hostney provides a pull-based migration system through a WordPress plugin that you install on your source site. The architecture is designed to handle the specific problems that make WordPress migrations unreliable.
The migration plugin
The Hostney Migration plugin is a WordPress plugin you install on the site you are migrating from. It exposes a secure REST API that Hostney’s infrastructure uses to pull your site data directly.
The process:
- Generate a migration token in the Hostney control panel for the destination site
- Install the plugin on your source WordPress site (download the latest release from GitHub)
- Paste the token in the plugin’s settings page (under Tools → Hostney Migration)
- Click connect – the plugin registers with Hostney, sending site metadata (WordPress version, PHP version, database size, file count) so the system can plan the migration
- Start the migration from the Hostney control panel
From this point, the migration runs server-to-server without your involvement. You can close the browser tab.
What happens during the migration
Database transfer. The plugin exposes database tables through its REST API. Hostney’s worker pulls rows in batches using primary key pagination – not a single massive dump. This means the migration works reliably regardless of database size, and if the connection drops mid-table, it resumes from the last row transferred rather than starting over.
File transfer. Files transfer in 2 MB chunks. The system scans the source filesystem, skips directories that do not need to migrate (cache directories, backup archives, debug logs, node_modules), and transfers everything else. A single file failure does not stop the migration – it is logged and the process continues.
URL replacement. After the database and files are at the destination, the system performs a serialization-aware search and replace to update all URLs from the source domain to the destination domain. This handles the serialized data problem described earlier automatically.
wp-config.php generation. A fresh configuration file is generated with the correct database credentials for the destination environment, while preserving your authentication keys and salts from the original.
Security of the migration
The migration token is a 96-character cryptographic token that expires after 24 hours. Every request from the worker to the plugin is signed with HMAC-SHA256 – the plugin verifies that each request genuinely comes from Hostney’s infrastructure and has not been tampered with. The plugin requires HTTPS, and it validates that connection requests are not coming from private IP ranges (preventing SSRF attacks).
Once the migration completes (or the token expires), the REST API endpoints stop accepting requests. You should uninstall the migration plugin from the source site after migration.
Migration without the plugin
If you cannot install plugins on your source host (some managed hosts restrict plugin installation), you can migrate via SSH. Provide your source server’s SSH credentials in the Hostney control panel, and the system connects directly to pull the database and files. The same chunked transfer, URL replacement, and configuration generation apply.
What the migration handles automatically
- Database export and import with foreign key handling
- Serialization-aware URL replacement across all tables
- File permissions set correctly for the container environment
- wp-config.php generated with correct database credentials
- Binary data (BLOBs) encoded correctly during transfer
- Large databases handled through paginated row batches
- WAF and security plugin interference bypassed transparently
- Real-time progress tracking with detailed logs
What you still need to do
- DNS switch. Point your domain to Hostney after verifying the migration (the control panel shows the destination IP)
- SSL certificate. SSL is provisioned automatically, but if you have a custom certificate, configure it through the control panel
- Email configuration. If your site sends email, verify delivery works on the new host. WordPress email configuration is independent of hosting
- Test the site. Use the hosts file trick described earlier to verify everything before switching DNS
- Uninstall the plugin. Remove the Hostney Migration plugin from the source site after migration
Common migration problems and how to avoid them
Mixed content warnings after migration
If your old site used HTTP and the new site uses HTTPS (or vice versa), images and resources loaded over the wrong protocol trigger mixed content warnings. The search-replace step should catch most of these, but hardcoded URLs in theme files, custom CSS, or JavaScript files will not be in the database and will not be replaced.
Fix: search your theme’s PHP, CSS, and JS files for hardcoded URLs and update them. Use protocol-relative URLs (
//domain.com/path
) or HTTPS URLs.
Missing images
If images appear broken after migration, check:
- The
wp-content/uploads/directory transferred completely - File permissions allow the web server to read the files
- The URL replacement updated
wp-content/uploadspaths correctly - If the old site used a CDN, image URLs may point to the CDN rather than the origin server
Permalink 404 errors
Permalinks work through URL rewriting. If every page except the homepage returns a 404:
- Nginx: Ensure
try_files $uri $uri/ /index.php?$args;is in the server configuration - Apache: Ensure
mod_rewriteis enabled and.htaccessis being read - Go to Settings → Permalinks and click Save to regenerate rewrite rules
Database connection errors
“Error establishing a database connection” means
wp-config.php
has incorrect database credentials, the database does not exist, or the MySQL service is not running. Verify the
DB_NAME
,
DB_USER
,
DB_PASSWORD
, and
DB_HOST
values match what you created at the destination.
Scheduled content and cron
WordPress cron jobs (scheduled posts, plugin scheduled tasks) sometimes break after migration because the cron system uses the site URL to trigger itself. If URLs were not updated correctly, or if the old cron entries reference the old domain, scheduled tasks may fail. Run
wp cron event list
to check pending cron events after migration.
Summary
WordPress migration involves moving four components – core files, wp-content, the database, and wp-config.php – and ensuring they work together at the destination. The critical steps that most migrations get wrong are the serialization-aware URL replacement (never use raw SQL REPLACE on a WordPress database) and testing before switching DNS.
Manual migration via SSH gives you full control and works for any source and destination. It requires command-line comfort and careful attention to each step. Migration plugins automate the process but vary in reliability – browser-dependent push-based plugins struggle with large sites, while server-to-server pull-based approaches handle the transfer more robustly.
Whatever method you use, the checklist is the same: backup first, transfer files and database, update configuration, replace URLs, test thoroughly, then switch DNS. Rushing the testing step is the most common cause of migration-day stress.