WordPress

WordPress Staging to Production: Database, Media and URLs

The part of WordPress deployment Git does not solve: moving database changes and media between staging and production without overwriting live orders.

5 min read
WordPressDeploymentWorkflow
971 words5 min read

The problem Git leaves behind

Code is easy: it lives in version control and deploys forward, local to staging to production. Everything else about a WordPress site lives in the database and the uploads folder, and those do not version, do not merge, and are being changed on production every day by orders, bookings, comments and content edits.

So the question on every WordPress project is: the client approved the new pages on staging, and there are a hundred new orders on production since staging was made. How do the pages get to production without losing the orders?

The rule: content flows backward

The default direction for the database is production to staging, never the reverse. Production is the truth. Staging is a disposable copy refreshed from it whenever you need to test against real data.

Pushing a staging database to production replaces everything that happened on production since staging was created. Orders, bookings, form entries, comments, user registrations, plugin logs. It is the single most destructive routine action in WordPress work, and it is offered as a one-click button on most managed hosts. Treat that button as dangerous.

So how do changes reach production?

Separate the kinds of change.

Structural changes: code. New post types, fields, templates, blocks, settings that can be expressed in code. These deploy with the theme and plugins. ACF field groups export to JSON in the theme; block patterns are files; options that matter can be set in code with add_filter('pre_option_*') or a configuration plugin. Nothing to move.

Content changes: recreate or migrate selectively. New pages and posts built on staging. Three approaches, in order of preference:

  1. Build them on production, hidden. Create the new pages as drafts or password-protected on production directly. The client reviews them there. Publish on launch day. No migration at all. For most content work, this is the answer, and staging is only used for the code changes that support it.

  2. Export and import the specific content. WordPress's built-in exporter produces WXR for chosen post types; the importer brings it into production. Media is fetched on import. Works for pages and posts; loses some meta depending on plugins. Fine for a handful of pages.

  3. Selective table or row migration. With WP Migrate or WP-CLI, push only specific tables (never wp_posts wholesale on a store) or specific post IDs. Fiddly, and only for large content jobs where option 1 is impractical.

Settings changes. Plugin options changed on staging. Write them down as you make them and repeat them on production. Or express them in code. The few minutes of manual repetition beats the risk of a database push.

Store or booking data. Never moves from staging to production. Ever.

The URL problem

Whenever a database does move, staging to local or production to staging, every URL in it is wrong. WordPress stores the site URL in options and in serialised data, in post content, in widget settings, in theme options, and plugins store it in places you have never heard of.

A plain SQL find-and-replace corrupts serialised PHP data, because the string length is stored alongside the string and no longer matches. The tool that handles this correctly is WP-CLI:

Text
wp search-replace 'https://example.com' 'https://staging.example.com' --all-tables --precise

--all-tables covers plugin tables outside the standard prefix. --precise handles serialised data at the cost of speed. Run --dry-run first to see the count. Migration plugins do the same thing internally.

Also after any database move: flush permalinks (wp rewrite flush), clear the object cache (wp cache flush), and check that the staging site has noindex set so a copy of production does not get crawled.

Media

Uploads are large and change constantly on production. Do not sync them to staging. Instead, make staging proxy them:

NGINX
location ~ ^/wp-content/uploads/(.*)$ {
  try_files $uri @production;
}
location @production {
  return 302 https://example.com$request_uri;
}

Or the equivalent .htaccess rewrite. Staging serves any image it has locally and falls back to production for the rest. Staging looks complete without holding a single copied file.

New media uploaded on staging as part of building new pages needs to come to production with those pages. The WXR export/import fetches it; a manual copy of the specific files into production's uploads works for a handful; option 1 above avoids the problem entirely.

For sites with large libraries, offloading media to object storage (S3 or equivalent) means every environment reads the same bucket and the question disappears.

A workable routine

  1. Refresh staging from production at the start of a piece of work: files and database, with search-replace, noindex, and the uploads proxy in place.
  2. Do code changes on local, deploy to staging via Git.
  3. Do content changes on production as drafts, or on staging with a plan for how each item reaches production.
  4. Note every settings change made on staging.
  5. Test on staging with the client.
  6. Deploy code to production. Repeat settings changes. Publish drafts. Import any exported content.
  7. Delete or refresh staging. It is out of date the moment production changes.

The hosts' one-click buttons

"Push staging to live" on managed hosts often offers to push files, database, or both, sometimes with table selection. Files-only pushes are usually safe if your code is otherwise under version control. Database pushes on any site with transactional data are not, regardless of what the host's interface implies. Read exactly what the button does before using it, and prefer the selective options.

Where this fits

This routine is part of how every WordPress project we run moves work into production, and it is the reason the projects do not lose orders. It also underpins the update cycle on a care plan: staging refreshed from production, updates tested, code and settings applied forward, database left alone.

All writingHire me for this