·SafeGuard Team

How to Create a WordPress Staging Site (And Actually Use It Safely)

What a staging site is, why most are set up wrong, and how SafeGuard's Quick and Full modes work, including the parts about indexing, access control, and the pre-push backup.

You're going to update WooCommerce. The release notes mention a database migration. You read the changelog three times trying to figure out if "non-breaking" means "won't break" or "we hope it won't break." It's a Tuesday afternoon. The site is live. People are checking out.

This is the moment a staging site exists for. You do the WooCommerce update on a copy first, in isolation, and confirm checkout still works. Then you push the change to production knowing it'll behave the same way it did in staging.

Most WordPress users don't have one because the existing options are a mess. Either you're paying for a managed host that gives you one (and you can't take it with you), or you're using an old plugin that creates a copy that's actually just a folder of your live files with a different URL, which means the staging "site" can break the live site if you delete the wrong thing.

This is what staging should look like, and how SafeGuard does it.

What a staging site actually is

A staging site is an isolated copy of your WordPress site where changes have no effect on production. The word "isolated" is doing a lot of work in that sentence:

  • Files should be separate, or at minimum read-only links from staging back to production. Staging shouldn't write to production.
  • Database should be a separate set of tables, or a separate database entirely. Edits in staging shouldn't appear in production rows.
  • URL should be different. staging.yoursite.com or a path under your live domain that's password protected.
  • Search engines should not index it. Otherwise you end up with two copies of your site in Google's index, splitting traffic and looking duplicate.

If any of these isn't true, you have a copy of your site, but you don't have a staging site.

Quick mode vs Full mode

SafeGuard offers two modes because they serve different needs.

Quick mode (~30 seconds)

Quick mode creates a staging environment that shares as much as possible with the live site, copying only what has to be different. Specifically:

  • WordPress core (wp-admin, wp-includes) is symlinked from production. Same WordPress version, same files, no duplication.
  • Themes and plugins are symlinked too. You can edit them in staging, but the file edits will surface in production because the file is the same file. This is intentional for the use case quick mode targets.
  • Uploads are symlinked read-only. Staging can read all your media, but uploads to staging go to a separate directory.
  • Database is a copy with a wp_sg_<id>_ prefix. Edits in staging touch staging tables only.

This mode finishes in about half a minute and uses almost no extra disk space. It's ideal for:

  • Testing a plugin update before activating it on the live site (you activate in staging, click around, then update in production).
  • Trying a CSS or theme tweak.
  • Verifying PHP version compatibility.
  • Checking what a settings change does without committing to it.

What it's not good for: anything that writes large amounts of data to disk, or any test where the staging site must continue working after the production files change.

Full mode (~2-5 minutes)

Full mode copies everything. Files are duplicated rather than symlinked. The database is a full table copy. The two sites are entirely independent: changes on one have zero effect on the other, including filesystem changes.

You want full mode for:

  • WooCommerce changes that involve order or product table migrations.
  • Any redesign that takes more than an hour, where production is going to keep changing during your work.
  • Long-running tests that need a stable staging state.
  • Handing the staging URL to a developer or client for review, where you don't want their actions to leak back to production.
  • Database schema migrations from a plugin update.

Full mode takes longer because it's actually copying gigabytes of files. The 2-5 minute range depends on site size; a small business site is closer to 2, a media-heavy site is closer to 5.

How creation works

There are two parts to creating a staging site: the files-and-DB part, and the routing part.

The files-and-DB part is what you'd expect. SafeGuard either symlinks or copies the WordPress directory into wp-content/safeguard-staging/<id>/. It dumps the production database, rewrites the table prefix from wp_ to wp_sg_<id>_, and imports it back. Serialized URLs get rewritten using the same unserialize-walk-reserialize approach migrations use, so Elementor sections and WooCommerce settings still work.

The routing part is where most other staging plugins go wrong. The question is: when someone visits the staging URL, how does WordPress know to load the staging directory and connect to staging tables instead of production?

SafeGuard installs a must-use plugin (mu-plugins/safeguard-staging-router.php) that runs before WordPress loads. It checks the request hostname and path. If they match a registered staging site, it sets the table prefix and the wp-content paths for that request only. The same WordPress installation serves both production and staging, but each request sees only the right data.

This works on both Apache and Nginx, on shared hosting, on managed hosts, and inside Docker. We tried four other approaches before settling on this one.

Access control: who can see your staging site

Staging sites should not be public. Three layers protect them by default:

  • HTTP Basic auth. SafeGuard generates a 12-character password (alphanumeric, no easily-confused characters), bcrypt-hashes it, and writes an .htpasswd for the staging path. The browser shows a username/password prompt before WordPress even loads. The plaintext password is shown to you exactly once at creation; we don't store it.
  • X-Robots-Tag: noindex, nofollow. Sent on every staging response. Google and Bing respect this. This alone isn't enough (some bots ignore it) which is why HTTP auth is the first line.
  • robots.txt with Disallow: / for the staging path, as a belt-and-suspenders measure for crawlers that read robots before fetching pages.

If you're sharing staging access with a developer or client, SafeGuard supports two staging roles:

  • Full Access: can create, edit, push to live.
  • Developer: can edit on the staging site, but cannot push changes to production.

You'd give a freelance Elementor designer the developer role. You'd give your senior dev full access. Everybody else gets nothing.

The push-to-live workflow

This is the part that decides whether staging is useful or just a copy you forget about.

When you click Push to Live, SafeGuard does this:

  1. Creates a full backup of the live site first. Not a "snapshot," not a "rollback point". It's an actual full backup, written to whatever storage you have configured. This is your safety net. The backup ID gets pinned as the rollback pointer.
  2. Copies the changed files from staging to production. Files that are symlinks (in quick mode) are skipped. Files unique to staging get copied, and files that exist in both get overwritten with the staging version.
  3. Imports the staging database tables into production, with the prefix rewritten from wp_sg_<id>_ back to the production prefix.
  4. Rewrites URLs during the database import using the unserialize-walk-reserialize approach, so the staging URL gets replaced with the production URL inside Elementor sections, WooCommerce settings, theme mods, etc.
  5. Cleans up the staging environment if you've asked it to.

If anything goes wrong during the push, you have the pre-push backup. Go to SafeGuard → Backups, find the one tagged "pre-push" with the staging ID, click Restore. You're back to where you were before the push, exactly.

We've never lost a customer's site to a push. The pre-push backup is why.

Auto-expiry

Staging sites should not live forever. Each one consumes disk space, and the longer it sits the more it diverges from production, making any eventual push more dangerous.

SafeGuard's default is to expire staging sites after 30 days. A daily cron checks for expired entries, runs the cleanup, removes the directory, drops the staging tables, and removes the routing entry. You can change the expiry, disable it, or extend a specific staging site that you need longer.

Things that go wrong, and how SafeGuard handles them

Search engines indexing the staging site. The combination of HTTP auth, X-Robots-Tag, and robots.txt means the staging URL never leaves the realm of "explicitly invited people." We've never had a staging URL show up in Google.

Pushing to live and breaking production. The pre-push backup makes this recoverable in one click. The unserialize-aware URL rewriting prevents the most common form of breakage (silent serialization corruption).

Disk filling up from forgotten staging sites. Auto-expiry handles this. The default is 30 days but you can shorten it.

The mu-router file getting deleted. If something removes mu-plugins/safeguard-staging-router.php, SafeGuard reinstalls it on the next request. If the file has been modified rather than deleted, the integrity check fires an admin notice asking you to investigate. Routing is too important to leave to "hopefully nobody touched the file."

WordPress version mismatches. Quick mode uses symlinks, so this is impossible (it's literally the same files). Full mode copies the version that was live at creation time, which is fine until you update WordPress in production and want to push staging changes through. SafeGuard detects the mismatch at push time and warns you to update staging first.

Try it

If you've ever broken your live site with a plugin update, you've earned the cost of setting up staging. SafeGuard makes it a 30-second click for most cases.

Get Early Access →