Skip to content
  • FG-58
  • 13 min read
  • Last checked November 8, 2026

How to Move WordPress to a New Host or Domain by Hand (Step by Step)

Move WordPress without a migration plugin: dump the database, copy the files, import, run a safe search-replace, test through your hosts file, then switch DNS.

Run the health check

Migration plugins time out on big sites, choke on archives over a few gigabytes, or are blocked by the new host. Moving WordPress by hand avoids all that, and it is the method professionals fall back on when a plugin fails. Done carelessly, though, it leaves a site that bounces visitors back to the old domain, loses widget and page builder settings, or shows the old host’s content for days.

Under the hood a move is four jobs: copy the files, copy the database, point the new copy at its new database, and, if the domain changes, rewrite every stored address without breaking serialized data. Then you test the new server privately before the world sees it, and only then change DNS. This guide walks through each job with the commands, for anyone comfortable with SSH or willing to learn.

What is happening

WordPress keeps its address in the database, not in the files. The siteurl and home rows in wp_options decide where WordPress thinks it lives, and it redirects visitors there. Full URLs are also stored inside post content, image links, menus, widget settings and page builder data. Many of those values are PHP serialized, a format that records the length of every string:

s:23:"https://old-example.com";

The s:23 means a string of 23 characters. If a plain SQL REPLACE() or a text editor swaps the domain for one of a different length, the count is wrong, PHP cannot read the value, and WordPress silently throws that whole setting away. That is why widgets, theme options and builder layouts vanish after careless moves. wp search-replace unpacks serialized values, edits them and repacks them with correct lengths.

Part Where it lives How it moves
Core, themes, plugins, uploads The site folder tar or rsync
Hidden files (.htaccess, .user.ini) The site folder tar or rsync (zip tools often skip them)
Content, settings, users, orders The database wp db export and wp db import
Database connection details wp-config.php Edited by hand on the new server
Domain to server mapping DNS Changed last

Before you start

  • Access you need: SSH on both the old and new servers (or SFTP plus phpMyAdmin as a slower fallback), the new host’s control panel, and login access to wherever your DNS is managed.
  • Backups: the export you make below is itself a backup. Keep it, untouched, until the new site has run cleanly for a week.
  • How to run commands. Commands starting with wp, tar, rsync or mysql run in an SSH terminal on the server named in each step, from the site folder (the one with wp-config.php). Some hosts call WP-CLI wp-cli instead of wp. Commands starting with dig or curl run on your own computer: macOS and Linux include them; on Windows use WSL, or curl.exe and Resolve-DnsName in PowerShell.

Step 1: Prepare both servers

Check versions first. Over SSH on each server:

wp core version
php -v
mysql --version

No placeholders. You want the new server’s PHP and database versions to be the same or newer. If the old site runs on MySQL 8 and the new one on MariaDB, expect the collation error covered in FG-59, and fix it during import. If the old PHP is much older, check that your theme and plugins support the new version before you move.

Next, lower the DNS TTL so the final switch is fast. On your computer:

dig +noall +answer example.com A

Replace example.com with your domain. +noall +answer trims the output to the answer lines. The number after the name is the TTL in seconds, for example example.com. 14400 IN A followed by the IP. Set it to 300 in your DNS panel, then wait at least the old TTL (here four hours) before cutover, so every resolver picks up the short value.

Step 2: Export the database on the old server

Over SSH on the old server, in the site folder:

wp db export ../site.sql --single-transaction --quick --default-character-set=utf8mb4
gzip ../site.sql

No placeholders; site.sql is just a file name. Writing to ../ keeps the dump out of the public web folder. wp db export reads the credentials from wp-config.php and passes extra flags to mysqldump: --single-transaction takes a consistent snapshot of InnoDB tables without locking the live site, --quick streams rows instead of loading whole tables into memory, and --default-character-set=utf8mb4 keeps emoji and accented characters intact. Good output is Success: Exported to '../site.sql'. and gzip then produces ../site.sql.gz.

No WP-CLI? Use mysqldump directly:

mysqldump -u DB_USER -p --single-transaction --quick --no-tablespaces --default-character-set=utf8mb4 DB_NAME | gzip > ../site.sql.gz

DB_USER and DB_NAME are the DB_USER and DB_NAME values in the old wp-config.php. -p prompts for the password so it does not land in your shell history. --no-tablespaces avoids the Access denied; you need (at least one of) the PROCESS privilege(s) error that MySQL 8 raises for normal users. The pipe into gzip compresses on the fly. A dump that ends with -- Dump completed on (check with zcat ../site.sql.gz | tail -1) is complete.

Step 3: Copy the files

The cleanest method is to pull from the new server with rsync, which copies directly between servers and can be rerun later to send only what changed. Over SSH on the new server, in the new, empty site folder:

rsync -avz --exclude 'wp-content/cache/' -e ssh OLD_USER@OLD_HOST:/OLD/PATH/TO/SITE/ ./
rsync -avz -e ssh OLD_USER@OLD_HOST:/OLD/PATH/TO/site.sql.gz ../

OLD_USER and OLD_HOST are your SSH login and server name or IP on the old host. /OLD/PATH/TO/SITE/ is the full path of the old site folder (run pwd there to see it). -a keeps permissions, timestamps and symlinks, -v lists files as they copy, and -z compresses in transit. The trailing slash on the source matters: with it, rsync copies the folder’s contents into ./; without it, you get an extra folder level. Excluding the cache folder avoids copying stale cached pages. Good output ends with a total size is line. Permission denied (publickey) means the new server cannot log in to the old one; add its SSH key or copy through your computer instead.

If the servers cannot talk to each other, make an archive on the old server with tar -czf ../files.tar.gz ., download it and upload it to the new one, then unpack with tar -xzf ../files.tar.gz in the new site folder. Either way, check that .htaccess arrived with ls -la.

Step 4: Create the database and user on the new host

On most hosting, use the control panel’s database section: create a database, create a user with a strong password, and give that user all privileges on that database. On your own server you can do it in SQL, logged in to mysql as an admin user:

CREATE DATABASE NEW_DB_NAME CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;
CREATE USER 'NEW_DB_USER'@'localhost' IDENTIFIED BY 'NEW_DB_PASSWORD';
GRANT ALL PRIVILEGES ON NEW_DB_NAME.* TO 'NEW_DB_USER'@'localhost';

NEW_DB_NAME, NEW_DB_USER and NEW_DB_PASSWORD are names and a password you choose. 'localhost' limits the user to connections from the same machine; if the database runs on a separate server or container, your host will tell you which host value to use. Good output is Query OK for each line.

Step 5: Point wp-config.php at the new database

Over SSH on the new server, in the site folder:

wp config set DB_NAME NEW_DB_NAME
wp config set DB_USER NEW_DB_USER
wp config set DB_PASSWORD 'NEW_DB_PASSWORD'
wp config set DB_HOST DB_HOST_VALUE
wp config get table_prefix

The first three values are from Step 4. DB_HOST_VALUE is usually localhost; some hosts give a hostname, an IP or a socket path instead, shown in the control panel. Single quotes around the password stop the shell from interpreting characters like $. wp config set edits the constants in wp-config.php without loading WordPress, so it works before the database exists. The last command prints the table prefix: it must match the prefix in your dump (look for CREATE TABLE `PREFIX_options` with zcat ../site.sql.gz | grep -m1 "CREATE TABLE").

While you are in wp-config.php, look for things tied to the old server: hard-coded WP_HOME and WP_SITEURL constants (update or remove them), a Redis or Memcached object-cache.php file in wp-content that expects a cache server the new host may not have, and absolute paths in .user.ini or security plugin settings such as auto_prepend_file.

Step 6: Import the database

Over SSH on the new server, in the site folder:

gunzip -c ../site.sql.gz | wp db import -
wp option get home

No placeholders. gunzip -c writes the uncompressed SQL to the pipe without creating a big file, and the - tells wp db import to read from that pipe. Good output is Success: Imported from 'STDIN'., then the old address. Errors such as MySQL server has gone away, Unknown collation or Row size too large are covered in the large import guide (FG-59).

If the domain is not changing (a pure host move), skip to Step 8.

Step 7: Rewrite the domain safely with search-replace

Always start with a dry run. Over SSH on the new server:

wp search-replace 'https://old-example.com' 'https://example.com' --all-tables-with-prefix --skip-columns=guid --precise --dry-run --report-changed-only

old-example.com is the old domain exactly as wp option get home showed it, and example.com is the new one. --all-tables-with-prefix includes plugin tables that WordPress does not register, such as form or builder tables. --skip-columns=guid leaves post GUIDs alone; they are permanent identifiers that feed readers use to tell old items from new, so rewriting them on an established site can make subscribers see every post as new. On a brand new site that has never had a feed, changing them does no harm. --precise forces the slower PHP path for every column instead of fast SQL, which handles awkward serialized data more reliably. --dry-run reports without saving, and --report-changed-only lists only tables with matches. WP-CLI also replaces the JSON-escaped form (https:\/\/old-example.com) automatically.

Good output is a table of tables and columns with replacement counts, ending in a Success: line that gives the total replacements to be made. Zero replacements usually means a typo or a protocol mismatch (http versus https, www versus no www). When the counts look right, run the same command without --dry-run. Then catch leftovers:

wp search-replace 'http://old-example.com' 'https://example.com' --all-tables-with-prefix --skip-columns=guid --precise
wp db search 'old-example.com' --all-tables-with-prefix --stats

The first line fixes old http:// links and upgrades them in one pass; repeat it for www. variants if the old site used them. wp db search lists remaining matches with a count per table. Look at what is left before replacing the bare domain: it may be email addresses such as info@old-example.com that should change only if the mailbox moved too.

On multisite, add --network and also update the domain columns in wp_blogs and wp_site and the DOMAIN_CURRENT_SITE constant; that is a larger job than this guide covers.

Step 8: Flush rewrite rules and caches

Over SSH on the new server:

wp rewrite flush --hard
wp cache flush
wp transient delete --all

No placeholders. --hard also rewrites .htaccess on Apache and LiteSpeed. wp cache flush empties the object cache, and the transient command clears cached values that may hold old URLs. If you use a page builder, run its own regenerate CSS or clear cache tool too, because builders write CSS files that contain absolute URLs. On Nginx, pretty permalinks need a try_files rule in the server config; if every page but the home page 404s, see FG-37.

Step 9: Test the new server through your hosts file

Your hosts file makes only your computer resolve the domain to the new server, so you can test the real address before anyone else sees it. Add one line:

NEW_SERVER_IP example.com
NEW_SERVER_IP www.example.com

NEW_SERVER_IP is the new server’s IPv4 address from the new host. The file is /etc/hosts on macOS and Linux (edit with sudo nano /etc/hosts) and C:\Windows\System32\drivers\etc\hosts on Windows (open Notepad as administrator). Flush your DNS cache afterward: ipconfig /flushdns on Windows, sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder on macOS. Browsers can keep their own cache, so test in a fresh private window. Remove the lines when you are done.

A quicker check that needs no file edits, on your computer:

curl -sI --resolve example.com:443:NEW_SERVER_IP https://example.com/

--resolve forces curl to connect to NEW_SERVER_IP for that name and port while still sending the right hostname. HTTP/2 200 is good. A 301 with a location: header pointing to the old domain means a missed replacement or a hard-coded constant (see FG-40). SSL certificate problem means the new server has no valid certificate for the name yet; many hosts can only issue a Let’s Encrypt certificate once DNS points at them, so add -k to skip verification for this test and ask the host about issuing the certificate at cutover.

In the browser, click through pages, log in, submit a form, check images and run a test checkout.

Step 10: Cut DNS over

  1. If anything changed on the old site since the export, repeat Step 2 and Step 6 (and Step 7) now, and rerun the Step 3 rsync, which copies only new or changed files such as fresh uploads.
  2. Change the A record (and www) to the new IP. If there is an AAAA record for the old server’s IPv6 address, update or delete it, or IPv6 visitors keep reaching the old host.
  3. Leave MX, SPF, DKIM and DMARC records alone unless email is moving too.
  4. Watch it propagate from your computer with dig +short example.com A @1.1.1.1 and dig +short example.com A @8.8.8.8. When both return the new IP, most visitors are on the new server.

For the redirect side of a domain change, use the dedicated guides rather than improvising: FG-38 builds the 301 map from old URLs to new ones, and FG-40 fixes a site that keeps sending visitors to the old domain. Mixed content warnings after the move are covered in FG-28.

Advanced: stream the whole move and do a delta cutover

On large sites you can skip the intermediate dump file entirely and pipe the database straight from server to server. Over SSH on the new server, in the site folder:

ssh OLD_USER@OLD_HOST "cd /OLD/PATH/TO/SITE && wp db export - --single-transaction --quick | gzip" | gunzip | wp db import -

The placeholders are the same as in Step 3. The part in quotes runs on the old server and writes a compressed dump to standard output, which travels over SSH and is unpacked and imported on the new one. Good output is the import success line. If the old host lacks WP-CLI in non-interactive shells, replace it with the full path from which wp.

For a near-zero downtime cutover on a busy site: do the full copy a day early and test it, then at cutover put the old site in maintenance mode, rerun the streamed import and rsync (which now moves only the day’s changes), run search-replace, and switch DNS. Keep the old site in maintenance mode, or have it serve a notice, so late visitors on stale DNS cannot place orders on a server that is no longer the source of truth.

When to contact your host

Open a ticket with the new host when you cannot create a database, the import is rejected for size or privileges, SSH between servers is blocked, or a certificate will not issue after DNS has switched. Send the domain, the new server IP, the exact command and full error text, UTC timestamps, your database server type and version on both sides, and the output of dig +short example.com A. If DNS is correct but you still see the old site, include the response headers from curl -sI. Use our diagnostic tools to confirm DNS and SSL from outside your own network.

Still stuck?

Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.

Get the free checklists

Keep reading

All field guides
FG-61 Migrations & backups

How to Recover a WordPress Site With No Backup

No backup? Check host snapshots, CDN caches, the Wayback Machine and old copies first, then rebuild from feeds and uploads, or rescue a database MySQL will not start.

Read the guide
16 min read

Search the field manual

What is going wrong?

Press / to search from any page, Esc to close