Technical guide

Setting up a WordPress staging environment on Linux

The simplest, safest and easiest to maintain way to test changes before they reach your live website.

Jump to the setup steps

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

Recommended: a 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:

Alternatives

OptionGood forTrade offs
Host built in staging (for example Kinsta, WP Engine, SiteGround, Cloudways)Managed hosting with a one click staging buttonOnly available on those hosts. Usually the easiest choice if you already have it.
WP Staging plugin (free and Pro)Quick tests without server accessStaging 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 serverNeeds SSH access and a little command line comfort, which this guide covers.

Prerequisites

Throughout this guide we use these example values. Replace them with your own.

ItemLiveStaging
Domainyourdomain.com.austaging.yourdomain.com.au
Folder/var/www/live/var/www/staging
Databasewp_livewp_staging
Database userwp_live_userwp_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;
Never reuse the live database user for staging. A separate user means a staging mistake physically cannot touch live data.

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 );
SettingLiveStaging
Database name, user, passwordLive valuesStaging values
WP_ENVIRONMENT_TYPEproductionstaging
WP_DEBUGOffOn, logged to file
CronOnOff
Salts and keysLive saltsOptional 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

sudo -u www-data wp plugin deactivate updraftplus wp-rocket woocommerce-gateway-stripe

Pushing changes back to live safely

Always back up live first. Never push a whole staging database over live on a site that takes orders, bookings, form entries or comments, because anything that happened on live since you cloned would be lost.

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 changedHow to push it
Theme, child theme or custom codeSync just that folder with rsync (below)
Plugin updatesTest on staging, then run the same update on live with wp plugin update
New pages or content editsRecreate 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 activityFull 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 oftenTask
Before every changeRun refresh-staging so you test against current live data
MonthlyUpdate plugins, themes and core on staging first, check the site, then repeat on live
MonthlyCheck wp-content/debug.log on staging for new warnings
QuarterlyConfirm the staging password still works and the site is still not indexed (search site:staging.yourdomain.com.au)
QuarterlyDelete old backups in /var/backups/wp and check free disk space
AutomaticCertbot 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

ProblemFix
Staging redirects to the live siteCheck 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 errorRead 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 liveConfirm wp-content/uploads was copied, then run search-replace for both the www and non www live URLs.
Permalinks give 404 errorsRun wp rewrite flush. On Apache, confirm AllowOverride All and a2enmod rewrite.
Certbot failsMake sure the DNS record has taken effect and port 80 is open (sudo ufw allow 'Nginx Full' or 'Apache Full').
Password prompt loopsCheck the path to /etc/staging.htpasswd and that the web server can read it.
Permission errors when updatingRun sudo chown -R www-data:www-data /var/www/staging and always run WP-CLI as www-data.
Import fails on a large databaseIncrease max_allowed_packet in MySQL, or import with mysql wp_staging < file.sql.
Need a hand? Fresh Digital can set up, refresh and maintain your staging environment for you, so you can test changes with confidence.