What staging is and why it matters
A staging site is a private copy of your live website. You use it to test plugin and theme updates, new pages, design changes and code before anything touches the site your customers see. If something breaks on staging, nothing is lost and nobody notices.
Our recommended approach
staging.yourdomain.com.au subdomain on the same Linux server as the live site, with its own folder, its own database and its own database user. WP-CLI handles cloning, URL replacement and syncing. Staging is protected with a password (HTTP basic auth) and told not to be indexed.Why this is the easiest to manage:
- No extra server to pay for or patch. The same PHP, web server and database versions as live, so what works on staging works on live.
- Separate files and database mean staging can never accidentally write to live data.
- WP-CLI turns a full refresh into a handful of repeatable commands, which we wrap in one script.
- Password protection keeps staging away from Google, customers and bots.
Alternatives
| Option | Good for | Trade offs |
|---|---|---|
| Host built in staging (for example Kinsta, WP Engine, SiteGround, Cloudways) | Managed hosting with a one click staging button | Only available on those hosts. Usually the easiest choice if you already have it. |
| WP Staging plugin (free and Pro) | Quick tests without server access | Staging lives inside the live install and database. Pushing back to live needs the Pro version. Heavier on large sites. |
| Subdomain plus WP-CLI (this guide) | Any VPS or dedicated Linux server | Needs SSH access and a little command line comfort, which this guide covers. |
Prerequisites
- A Linux server running Ubuntu 22.04 or 24.04, or Debian 12, with SSH access as a user with
sudo. - Nginx or Apache, MySQL 8 or MariaDB 10.6 or newer, and PHP 8.1 or newer, already serving the live site.
- Access to your DNS provider to add a record for the staging subdomain.
- A recent, tested backup of the live site.
- About 2 to 3 times the live site's disk usage free (check with
df -handdu -sh /var/www/live).
Throughout this guide we use these example values. Replace them with your own.
| Item | Live | Staging |
|---|---|---|
| Domain | yourdomain.com.au | staging.yourdomain.com.au |
| Folder | /var/www/live | /var/www/staging |
| Database | wp_live | wp_staging |
| Database user | wp_live_user | wp_staging_user |
Step by step setup
1. Point the subdomain at your server
In your DNS provider, add an A record for staging pointing to the same IP address as your live site. Allow a few minutes for it to take effect, then check:
dig +short staging.yourdomain.com.au
2. Install WP-CLI
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
php wp-cli.phar --info
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
wp --info
3. Create the staging database and user
sudo mysql
CREATE DATABASE wp_staging CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wp_staging_user'@'localhost' IDENTIFIED BY 'a-long-unique-password';
GRANT ALL PRIVILEGES ON wp_staging.* TO 'wp_staging_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
4. Copy the files
sudo mkdir -p /var/www/staging
sudo rsync -a --exclude 'wp-content/cache/' /var/www/live/ /var/www/staging/
sudo chown -R www-data:www-data /var/www/staging
5. Copy the database
cd /var/www/live
sudo -u www-data wp db export /tmp/live.sql
cd /var/www/staging
sudo -u www-data wp config set DB_NAME wp_staging
sudo -u www-data wp config set DB_USER wp_staging_user
sudo -u www-data wp config set DB_PASSWORD 'a-long-unique-password'
sudo -u www-data wp db import /tmp/live.sql
rm /tmp/live.sql
6. Replace the live URL with the staging URL
WP-CLI handles serialised data safely, which a plain text find and replace in SQL does not.
cd /var/www/staging
sudo -u www-data wp search-replace 'https://yourdomain.com.au' 'https://staging.yourdomain.com.au' --all-tables --precise --dry-run
sudo -u www-data wp search-replace 'https://yourdomain.com.au' 'https://staging.yourdomain.com.au' --all-tables --precise
sudo -u www-data wp search-replace 'https://www.yourdomain.com.au' 'https://staging.yourdomain.com.au' --all-tables --precise
sudo -u www-data wp cache flush
7. Create a password for staging
sudo apt install -y apache2-utils
sudo htpasswd -c /etc/staging.htpasswd genesisfx
8a. Web server config: Nginx
Create /etc/nginx/sites-available/staging.yourdomain.com.au. Copy your live server block as a starting point so PHP settings match, then adjust it to look like this:
server {
listen 80;
server_name staging.yourdomain.com.au;
root /var/www/staging;
index index.php;
auth_basic "Staging";
auth_basic_user_file /etc/staging.htpasswd;
add_header X-Robots-Tag "noindex, nofollow" always;
location / { try_files $uri $uri/ /index.php?$args; }
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location ~ /\.ht { deny all; }
}
sudo ln -s /etc/nginx/sites-available/staging.yourdomain.com.au /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Match php8.3-fpm.sock to the version in your live config.
8b. Web server config: Apache
Create /etc/apache2/sites-available/staging.yourdomain.com.au.conf:
<VirtualHost *:80>
ServerName staging.yourdomain.com.au
DocumentRoot /var/www/staging
<Directory /var/www/staging>
AllowOverride All
AuthType Basic
AuthName "Staging"
AuthUserFile /etc/staging.htpasswd
Require valid-user
</Directory>
Header always set X-Robots-Tag "noindex, nofollow"
</VirtualHost>
sudo a2enmod headers rewrite
sudo a2ensite staging.yourdomain.com.au
sudo apache2ctl configtest && sudo systemctl reload apache2
9. Add a free SSL certificate
sudo apt install -y certbot python3-certbot-nginx # or python3-certbot-apache
sudo certbot --nginx -d staging.yourdomain.com.au # or --apache
sudo certbot renew --dry-run
Visit https://staging.yourdomain.com.au. You should be asked for the staging password, then see a copy of your site.
wp-config differences
Add these lines to /var/www/staging/wp-config.php, above the line that says That's all, stop editing!:
define( 'WP_ENVIRONMENT_TYPE', 'staging' );
define( 'WP_HOME', 'https://staging.yourdomain.com.au' );
define( 'WP_SITEURL', 'https://staging.yourdomain.com.au' );
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true ); // writes to wp-content/debug.log
define( 'WP_DEBUG_DISPLAY', false );
define( 'DISABLE_WP_CRON', true ); // stops scheduled tasks such as emails and syncs
define( 'WP_CACHE', false );
| Setting | Live | Staging |
|---|---|---|
| Database name, user, password | Live values | Staging values |
WP_ENVIRONMENT_TYPE | production | staging |
WP_DEBUG | Off | On, logged to file |
| Cron | On | Off |
| Salts and keys | Live salts | Optional fresh salts with wp config shuffle-salts |
Blocking indexing and emails on staging
Stop search engines
The password and X-Robots-Tag header already do the heavy lifting. Also tick the WordPress setting as a belt and braces measure:
cd /var/www/staging
sudo -u www-data wp option update blog_public 0
Stop emails going to real customers
This matters most for WooCommerce, booking and membership sites. Install a mail catcher so every email is logged instead of sent:
sudo -u www-data wp plugin install wp-mail-logging --activate
sudo -u www-data wp plugin install disable-emails --activate
Turn off anything that talks to the outside world
- Switch payment gateways (Stripe, PayPal) to test mode or deactivate them.
- Deactivate backup plugins that push to cloud storage, so staging does not overwrite live backups.
- Pause CRM, Mailchimp, analytics and marketing pixel integrations.
- Deactivate CDN and caching plugins unless you are specifically testing them.
sudo -u www-data wp plugin deactivate updraftplus wp-rocket woocommerce-gateway-stripe
Pushing changes back to live safely
Step 1: back up live
cd /var/www/live
sudo mkdir -p /var/backups/wp
sudo -u www-data wp db export - | gzip | sudo tee /var/backups/wp/live-$(date +%F-%H%M).sql.gz > /dev/null
sudo tar -czf /var/backups/wp/live-files-$(date +%F-%H%M).tar.gz -C /var/www live
Step 2: choose the right type of push
| What changed | How to push it |
|---|---|
| Theme, child theme or custom code | Sync just that folder with rsync (below) |
| Plugin updates | Test on staging, then run the same update on live with wp plugin update |
| New pages or content edits | Recreate on live, or export and import the specific pages with WordPress's Tools, Export and Import |
| A full redesign on a brochure site with no live activity | Full database push (below), during a quiet period |
Push theme or code
sudo rsync -a --delete --dry-run /var/www/staging/wp-content/themes/your-theme/ /var/www/live/wp-content/themes/your-theme/
sudo rsync -a --delete /var/www/staging/wp-content/themes/your-theme/ /var/www/live/wp-content/themes/your-theme/
cd /var/www/live && sudo -u www-data wp cache flush
Full database push (only when safe)
cd /var/www/staging
sudo -u www-data wp db export /tmp/staging.sql
cd /var/www/live
sudo -u www-data wp maintenance-mode activate
sudo -u www-data wp db import /tmp/staging.sql
sudo -u www-data wp search-replace 'https://staging.yourdomain.com.au' 'https://yourdomain.com.au' --all-tables --precise
sudo -u www-data wp option update blog_public 1
sudo -u www-data wp cache flush
sudo -u www-data wp maintenance-mode deactivate
rm /tmp/staging.sql
Then re-enable any plugins you turned off on staging (payments, backups, email), and check the home page, contact forms and checkout.
Rolling back
cd /var/www/live
gunzip -c /var/backups/wp/live-YYYY-MM-DD-HHMM.sql.gz | sudo -u www-data wp db import -
Refreshing staging from live
Save this as /usr/local/bin/refresh-staging and run sudo chmod +x /usr/local/bin/refresh-staging. Running sudo refresh-staging gives you a fresh copy of live in a minute or two.
#!/bin/bash
set -euo pipefail
LIVE=/var/www/live
STG=/var/www/staging
LIVE_URL=https://yourdomain.com.au
STG_URL=https://staging.yourdomain.com.au
rsync -a --delete --exclude 'wp-config.php' --exclude 'wp-content/cache/' "$LIVE/" "$STG/"
chown -R www-data:www-data "$STG"
sudo -u www-data wp --path="$LIVE" db export /tmp/refresh.sql
sudo -u www-data wp --path="$STG" db reset --yes
sudo -u www-data wp --path="$STG" db import /tmp/refresh.sql
rm /tmp/refresh.sql
sudo -u www-data wp --path="$STG" search-replace "$LIVE_URL" "$STG_URL" --all-tables --precise
sudo -u www-data wp --path="$STG" option update blog_public 0
sudo -u www-data wp --path="$STG" plugin activate disable-emails wp-mail-logging || true
sudo -u www-data wp --path="$STG" cache flush
echo "Staging refreshed from live."
Excluding wp-config.php keeps the staging database details and settings in place.
Routine maintenance
| How often | Task |
|---|---|
| Before every change | Run refresh-staging so you test against current live data |
| Monthly | Update plugins, themes and core on staging first, check the site, then repeat on live |
| Monthly | Check wp-content/debug.log on staging for new warnings |
| Quarterly | Confirm the staging password still works and the site is still not indexed (search site:staging.yourdomain.com.au) |
| Quarterly | Delete old backups in /var/backups/wp and check free disk space |
| Automatic | Certbot renews SSL on its own. Check with sudo certbot certificates |
Handy update commands:
cd /var/www/staging
sudo -u www-data wp core update
sudo -u www-data wp plugin update --all
sudo -u www-data wp theme update --all
sudo -u www-data wp core verify-checksums
Troubleshooting
| Problem | Fix |
|---|---|
| Staging redirects to the live site | Check WP_HOME and WP_SITEURL in wp-config, rerun search-replace, and clear any caching plugin and browser cache. |
| "Error establishing a database connection" | Check DB_NAME, DB_USER and DB_PASSWORD in staging wp-config, and test with sudo -u www-data wp db check. |
| White screen or 500 error | Read wp-content/debug.log and the web server error log (/var/log/nginx/error.log or /var/log/apache2/error.log). Deactivate plugins with wp plugin deactivate --all to isolate the cause. |
| Images missing or pointing to live | Confirm wp-content/uploads was copied, then run search-replace for both the www and non www live URLs. |
| Permalinks give 404 errors | Run wp rewrite flush. On Apache, confirm AllowOverride All and a2enmod rewrite. |
| Certbot fails | Make sure the DNS record has taken effect and port 80 is open (sudo ufw allow 'Nginx Full' or 'Apache Full'). |
| Password prompt loops | Check the path to /etc/staging.htpasswd and that the web server can read it. |
| Permission errors when updating | Run sudo chown -R www-data:www-data /var/www/staging and always run WP-CLI as www-data. |
| Import fails on a large database | Increase max_allowed_packet in MySQL, or import with mysql wp_staging < file.sql. |
