Himanshu SharmaSoftware engineer
Let’s talk

WordPress · Lesson 17 of 45

Moving a site from local to live

Move a WordPress site between local, staging and live safely, with files, a database export and URL search-replace, and a simple workflow for changes.

13 minWP-CLI and workflowUpdated 23 October 2026

By the end of this lessonMove a whole site to new hosting or a new address without broken links, and follow a safe local, staging and live workflow.

What makes up a site

To move a WordPress site you need two things:

  1. Files: WordPress core (which you can download again), and the parts that are yours: wp-content (themes, plugins, uploads) and wp-config.php.
  2. The database: every post, page, setting, user and comment.

Copying only one of them gives you a broken site.

The address is stored in the database

WordPress stores its own address in the database, in more places than you might expect: the siteurl and home options, every link and image address inside post content, menu items, widget and plugin settings. Here our local site's address appears in content, as images and internal links normally do:

bash
wp post create --post_title="Our bakery" --post_status=publish --post_content="<img src=\"http://localhost:8080/wp-content/uploads/bakery.jpg\" alt=\"Bakery\"> See our <a href=\"http://localhost:8080/menu/\">menu</a>."
wp option update bakery_settings '{"logo":"http://localhost:8080/wp-content/uploads/logo.png","hours":"9-6"}' --format=json
wp option get bakery_settings --format=json
result
Success: Created post 4.
Success: Updated 'bakery_settings' option.
{"logo":"http:\/\/localhost:8080\/wp-content\/uploads\/logo.png","hours":"9-6"}

The option was stored as a PHP array, so in the database it is serialized, a text format that records each string's length:

bash
wp db query "SELECT option_value FROM wp_options WHERE option_name = 'bakery_settings'" --skip-column-names
result
a:2:{s:4:"logo";s:50:"http://localhost:8080/wp-content/uploads/logo.png";s:5:"hours";s:3:"9-6";}

s:50:"http://localhost:8080/..." means "a string of 50 characters". If you replaced the address with https://bakery.example (a different length) using plain SQL, the recorded length would be wrong, PHP could no longer read the data, and the settings would silently vanish. That is why you must use a serialization-aware tool.

Changing the address safely

wp search-replace understands serialized data and fixes the lengths. Always run it with --dry-run first:

bash
wp search-replace "http://localhost:8080" "https://bakery.example" --dry-run --report-changed-only
result
+------------+--------------+--------------+------+
| Table      | Column       | Replacements | Type |
+------------+--------------+--------------+------+
| wp_options | option_value | 3            | PHP  |
| wp_posts   | post_content | 3            | SQL  |
| wp_posts   | guid         | 4            | SQL  |
| wp_users   | user_url     | 1            | SQL  |
+------------+--------------+--------------+------+
Success: 11 replacements to be made.

The report shows how many replacements each table and column would get. In wp_options: the siteurl and home options and our settings option (marked PHP because it was serialized data, handled specially). In wp_posts: links in post content (our post and the sample page, which links to the dashboard) and each post's guid column. And the administrator's website in wp_users.

The guid column is a permanent identifier that feed readers use to recognise posts. When moving a local site to its first live address, replacing it is harmless. On a site that is already live and has subscribers, many people add --skip-columns=guid so existing feed items are not seen as new. Without --dry-run the changes are made:

bash
wp search-replace "http://localhost:8080" "https://bakery.example" --report-changed-only
wp option get home
wp option get bakery_settings --format=json
result
+------------+--------------+--------------+------+
| Table      | Column       | Replacements | Type |
+------------+--------------+--------------+------+
| wp_options | option_value | 3            | PHP  |
| wp_posts   | post_content | 3            | SQL  |
| wp_posts   | guid         | 4            | SQL  |
| wp_users   | user_url     | 1            | SQL  |
+------------+--------------+--------------+------+
Success: Made 11 replacements.
https://bakery.example
{"logo":"https:\/\/bakery.example\/wp-content\/uploads\/logo.png","hours":"9-6"}

The serialized option now holds the new address with correct lengths, so it still reads back as normal data.

The full move, step by step

Moving from your computer to a live server:

  1. Prepare the server: create a database and a database user on the host, and point the domain at it.
  2. Export the database locally: wp db export site.sql.
  3. Upload the files: wp-content at least (or the whole folder) with SFTP, rsync or your host's file manager.
  4. Import the database on the server: wp db import site.sql (or phpMyAdmin).
  5. Edit wp-config.php on the server with the new database name, user, password and host, and fresh security keys from https://api.wordpress.org/secret-key/1.1/salt/.
  6. Search-replace the old address with the new one: wp search-replace "http://localhost:8080" "https://www.example.com" --dry-run, then without --dry-run.
  7. Flush rewrite rules and caches: wp rewrite flush, wp cache flush.
  8. Check: log in, click through pages, test forms, look for mixed-content warnings (images still on http://), and untick Discourage search engines if it was set.

Many people use a migration plugin (such as All-in-One WP Migration, Duplicator or Migrate Guru) or their host's migration tool, which bundle these steps. Knowing the manual steps means you can fix things when an automated move fails part-way, which happens with large sites.

Local, staging and live

For sites that matter, use three copies:

  • Local: on your computer, for building and experimenting.
  • Staging: a private copy on the server (many hosts create one with a click), for testing updates and new features with real data.
  • Live (production): what visitors see.

The golden rule: code moves up, content moves down.

  • Theme and plugin code goes local → staging → live, ideally through Git and a deployment step.
  • Content (posts, orders, comments, users) is created on live, so you copy the live database down to staging or local when you need fresh data.

Never copy a staging database up over a live shop or membership site: you would erase every order and sign-up made since the copy was taken.

HTTPS

Live sites must use HTTPS. Most hosts provide free certificates (Let's Encrypt). After enabling it, make sure both addresses in Settings → General start with https://, and search-replace any remaining http:// links to your own domain.

Common mistakes

  • Plain SQL REPLACE() on URLs, corrupting serialized settings.
  • Forgetting wp-config.php changes on the new server.
  • Overwriting live content with an older database.
  • Leaving "Discourage search engines" on after launch.
  • No backup before the move.

Exercise

Your local site at http://mysite.local must move to https://www.mysite.com. Write the WP-CLI commands for the database part on the server, after the files and database have been uploaded and imported.

Show one possible answer
bash
wp search-replace "http://mysite.local" "https://www.mysite.com" --dry-run --report-changed-only
wp search-replace "http://mysite.local" "https://www.mysite.com" --report-changed-only
wp rewrite flush
wp cache flush
wp option get home

The last command should print https://www.mysite.com. Then log in at https://www.mysite.com/wp-admin/ and click through the site.

Key points

  • A WordPress site is files (mainly wp-content and wp-config.php) plus the database; moving it means copying both.
  • The database stores the site address in many places, including serialized data, so use a serialization-aware search-replace.
  • Professional workflows use local, staging and live copies: code moves up, content moves down.
  • Migration plugins and host tools automate the process; understanding the manual steps helps when they fail.

Check yourself

1. What must be copied to move a WordPress site?
2. Why not change URLs with a plain SQL REPLACE?
3. In a local, staging, live workflow, which direction does content usually move?
4. What is a staging site?