Why a staging site rather than testing on the live site
A form plugin that no longer initialises, a theme that loses its layout, the screen "There has been a critical error on this website": these failures follow an update that looked harmless. With three sites to manage, caution is enough. With thirty, you either update in the dark or stop updating, and then the unpatched vulnerability is what costs you.
A staging site is a live copy of the site, on a separate address, with its own database. You can break what you like, read the errors, fix them, then replay the already validated step on production. It does not replace the backup: it makes restoring one rarely necessary.
What a staging site gives you
- Updates without surprises — every core, theme or plugin change is seen on screen before it reaches visitors.
- A readable diagnosis — errors go to a log, not in front of the client, and the cause is found before they call.
- A shared review — the client or their project manager signs off on a separate address, without touching the public site.
- A change log — you note the version before and after for each plugin, and replay exactly the same on production.
- A trial for new things — new theme, new page builder, new payment module: everything is judged on a copy.
- A prepared rollback — the backup is made and checked before the test, not after the incident.
Prerequisites, with numbers, before copying
Count three times the size of the live site in disk space: the site, its copy and the database export. Read the size of wp-content with du -sh wp-content and that of the database in phpMyAdmin or with wp db size. Plan one subdomain per client, for example staging.your-domain.com, and the same PHP version as production: a staging site on a newer PHP validates a configuration that is not the client's. That version is shown under Tools › Site Health › Info, and the versions still supported are kept up to date at php.net/supported-versions.php.
You also need a terminal with wp-cli (SSH on an account that allows it, or a VPS). This guide describes the manual copy, files then database, which shows exactly what is copied. If WP Toolkit is available on your account, its cloning produces that copy for you (see the guide "Install WordPress from cPanel"); the rest of the guide (noindex, authentication, emails switched off, tests) then applies unchanged. For installing a new site from cPanel, see the dedicated guide.
Build the staging site, step by step
Back up production before any write
From the live site's folder:
mkdir -p ~/preprod-backupswp db export ~/preprod-backups/before-staging.sql --single-transactiontar -czf ~/preprod-backups/files-before-staging.tar.gz -C ~/public_html .
Check that both files exist and are not empty withls -lh ~/preprod-backups. The plan's own backups do not replace this step: their frequency depends on the plan, so they may be several days old.Create the subdomain and its folder
Create
staging.your-domain.comin cPanel (the guide "Create a subdomain in cPanel" details the screen) with its own document root, preferably outsidepublic_html, for example~/staging.your-domain.com. A folder placed insidepublic_htmlwould also be reachable through an address of the live site. Wait until the subdomain's TLS certificate is issued before testing.Copy the files
rsync -a --exclude='wp-content/cache/' ~/public_html/ ~/staging.your-domain.com/
Adapt the source path to the client site's document root. Without a terminal, the cPanel file manager lets you compress the folder, move it, then extract it into the subdomain's folder.Create a dedicated database and user
In the MySQL databases section of cPanel, create a new database and a user, and give it all privileges on that database only. Then open the copy's
wp-config.phpand changeDB_NAME,DB_USERandDB_PASSWORD. This is the costliest mistake in this guide: copyingwp-config.phpwithout changing those three lines makes the staging site write into the production database.Import the database
From the copy's folder:
cd ~/staging.your-domain.comwp db import ~/preprod-backups/before-staging.sqlwp option get siteurl
The last command should still show the production address: that is normal, the URLs have not been replaced yet. If it fails,wp-config.phpstill points to the wrong database.Replace the URLs, as a dry run first
wp search-replace 'https://your-domain.com' 'https://staging.your-domain.com' --all-tables --skip-columns=guid --dry-run
Read the report: it lists the tables and the number of replacements. Then run the same command without--dry-run. WordPress stores many settings as serialized data, where each string carries its length: asedon the export changes the URL length without fixing that count, and the settings become unreadable.wp search-replaceunderstands this data and rewrites it correctly.Set the staging settings in wp-config.php
Add, above the line "That's all, stop editing":
define('WP_ENVIRONMENT_TYPE', 'staging');define('DISABLE_WP_CRON', true);define('DISALLOW_FILE_EDIT', true);define('WP_DEBUG', true);define('WP_DEBUG_LOG', true);define('WP_DEBUG_DISPLAY', false);
Errors go towp-content/debug.logwithout showing to visitors.WP_ENVIRONMENT_TYPEhas existed since WordPress 5.5 and acceptslocal,development,stagingandproduction: it lets themes and plugins adapt, but it does not hide the site from search engines, hence the next step.Close access and request noindex
Under Settings › Reading, tick "Discourage search engines from indexing this site", or from the command line:
wp option update blog_public 0. Then add HTTP authentication to the copy's folder: a.htpasswdfile created withhtpasswd -c ~/.htpasswd-staging client, then in the folder's.htaccessthe directivesAuthType Basic,AuthName "Preproduction",AuthUserFilepointing to that file andRequire valid-user. Failing that, restrict access to an IP list.Check the copy the way a robot would
curl -sI https://staging.your-domain.com/
The response should be a401. Then, with the credentials:curl -s -u client https://staging.your-domain.com/ | grep -i robots
You should see ameta name='robots'tag containingnoindex. If a cache or SEO plugin replaces it, that is the one to configure. Finally, check that the theme contains no hard-coded production address:grep -r "https://your-domain.com" wp-content/themes.Attach staging to your agency routine
Turn this setup into a template: one subdomain per client, in the client's cPanel account, with the same checklist. The order of operations for moving a site to another host is covered by the guide "Migrate a client WordPress site without downtime"; the staging site described here is a lasting tool, not a cutover step.
Really isolate it: noindex is not enough
Noindex asks search engines not to keep a page; it stops nobody from reading it. A staging site open to everyone ends up in a link pasted in a ticket, in a sitemap or in a screenshot, and it then publishes content duplicated from production. The two protections complement each other: noindex tells engines not to index, HTTP authentication or an IP list forbids access.
One trap: if you block the subdomain with robots.txt, a robot no longer reads the page and therefore never sees the noindex tag. With authentication in place, that file is of no use.
Neutralise emails, payments and scheduled tasks
The copy contains the site's real customers: subscribers, buyers, order addresses. Everything that sends itself must be switched off before the slightest test, otherwise an abandoned-cart reminder, an appointment reminder or a subscription renewal goes out twice from the copy. WordPress scheduled tasks are triggered by visits: on staging, disable them with DISABLE_WP_CRON and run them by hand with wp cron event run --due-now when you want to test them.
Checklist before touching a single plugin
- Emails — sending plugin in test mode or redirected to a review mailbox, shop notifications switched off.
- Payments — gateway in test mode, production keys removed from the copy, callback addresses (webhooks) not declared for staging.
- Scheduled tasks —
DISABLE_WP_CRONset, pending events listed withwp cron event list. - File editing —
DISALLOW_FILE_EDITactive so code cannot be edited from the admin. - Error log —
WP_DEBUG_LOGactive andWP_DEBUG_DISPLAYset tofalse. - Third-party integrations — CRM, directories, analytics tools and production API keys replaced or removed.
- Search engines — noindex set and authentication in place, checked with
curlas above.
Test updates, one at a time
List what is waiting for an update with wp plugin list --update=available. Then update one plugin at a time: wp plugin update plugin-name, browse the pages that matter (home, contact form, cart, login) and read the end of the log with tail -n 30 wp-content/debug.log. Move on to the next one only when the log is quiet. Do the same for the theme with wp theme update theme-name, then for core with wp core update.
In a change log, note for each line the version before and the version after. That is the list you will replay on production, version by version, instead of copying anything.
When the update breaks: reading debug.log
The classic symptom is the screen "There has been a critical error on this website". WordPress then enters recovery mode and sends a link to the admin address, which assumes email sending works: on staging it is neutralised, so do not wait for it. The cause is in the log: tail -n 40 wp-content/debug.log. Look for the last line starting with PHP Fatal error, such as Call to undefined function or Cannot declare class, and note the file path: the folder under wp-content/plugins/ names the faulty plugin. Deactivate it without going through the admin using wp plugin deactivate plugin-name, and the site comes back and you know who to blame.
Troubleshooting: the other symptoms you really meet
- The site redirects to production —
wp option get siteurlandwp option get homestill show the old address, or a page builder keeps hard-coded URLs; run the replacement again, with--preciseif needed. - "Error establishing a database connection" — the copy's
DB_NAME,DB_USER,DB_PASSWORDorDB_HOSTare wrong, or the user has no rights on the new database. - Images or styles loaded from production — escaped URLs, such as
https:\/\/in page builders' JSON data, escaped the replacement; run it again with that form. - Login loop or immediate logout — the cookies belong to the old domain; clear them and check
siteurlandhome. - The staging site shows up in a search engine — noindex was set too late or a plugin overrides it; switch to HTTP authentication and request removal of the address.
Files or database: what moves, and how
Scroll the table
| Item | Where it lives | How to move it to production |
|---|---|---|
| Theme and child theme | Files | Deploy the validated folder, or replay `wp theme update` at the tested version |
| Plugins | Files | Replay `wp plugin update plugin-name --version=X` with the tested version |
| Must-use plugins (mu-plugins) | Files | Copy the validated file, with the old one saved alongside |
| Uploaded media (uploads) | Files | Never push from staging to production: production keeps receiving new media |
| Plugin settings | Database (options) | Redo the setting by hand on production, or `wp option update`, naming the option |
| Orders, forms, accounts, comments | Database | Never overwrite: this data lives on the production side |
| Tables created by an update | Database | Let the plugin create them by replaying the update on production |
Moving to production, and being able to go back
On a transactional site, never import the staging database into production: during your tests, orders, sign-ups and form entries arrived online, and an import erases them. Replay the validated changes instead. Sequence: a fresh backup (wp db export and a files archive, as in step 1), wp maintenance-mode activate, updates in order and at the versions in the log, a check of the key pages, then wp maintenance-mode deactivate.
To go back, restore the files first: wp plugin install plugin-name --version=old --force or the archive from step 1. Restore the database only as a last resort, and only if the update changed tables: everything entered since the backup would be lost, so the export must then be only minutes old. Keep the backup until the client has approved the live site.
The hardening reflex
Once the update is validated, delete the copy of the production database left in ~/preprod-backups and on staging: a full export holds customers' addresses, orders and password hashes. Never leave WP_DEBUG on for the live site, the log can contain paths and queries.
A routine that holds at portfolio scale
A staging site is judged by its discipline: always the same setup, the same checklist, the same log. For an agency, the routine fits in a procedure template: one subdomain per client, a dedicated database, noindex plus authentication, emails switched off, updates one at a time, replay to deploy. It works all the better when each client site lives in its own account, with its own backups and separate access.