
A staging site is where you break things on purpose. That way, your production WordPress site doesn’t break by accident. This WordPress staging site tutorial shows you how to clone a live WordPress install onto the same VPS (or dedicated server), put it on its own subdomain, and keep it private with HTTP basic auth and Let’s Encrypt SSL.
The workflow is simple. Copy files and the database, rewrite URLs safely, disable outbound email, and block indexing.
Then test updates and plugins. Once it’s in place, you’ll have a repeatable checklist for fresh staging copies.
What you’ll build (and why this staging pattern works)
You’ll create staging.example.com that runs the same theme and plugins as production, but isn’t publicly accessible.
Hosting staging on the same server removes hidden differences. PHP versions, system libraries, file permissions, and image tooling stay consistent.
- Separate Nginx server block for staging (distinct root directory)
- Separate database (prevents accidental writes to production)
- HTTPS via Let’s Encrypt
- HTTP basic auth to keep bots and clients out
- Email disabled so staging can’t spam users
- Search engines blocked to avoid duplicate content
If you’d rather not maintain the stack yourself, managed VPS hosting from HostMyCode can cover OS and web server upkeep, security updates, and monitoring while you focus on WordPress.
Prerequisites for this WordPress staging site tutorial
- Ubuntu 24.04/26.04 VPS (or a dedicated server) with root SSH access
- Nginx installed and serving production WordPress (PHP-FPM configured)
- A DNS record you control for
staging.example.com - Certbot available (we’ll install if needed)
If you’re still tightening SSH and firewall defaults, keep your baseline clean with: VPS hardening tutorial (2026).
Step 1: Create DNS for the staging subdomain
Point staging.example.com at your server’s IP.
- A record:
staging→YOUR_SERVER_IP - AAAA record (optional):
staging→YOUR_IPV6
If your domain is managed elsewhere, you can still simplify things by consolidating DNS through domains and DNS at HostMyCode.
Step 2: Prepare directories and permissions for staging
Use a layout you can recognize at a glance. In this example, production lives at /var/www/example.com. Staging lives at /var/www/staging.example.com.
sudo mkdir -p /var/www/staging.example.com/public
sudo mkdir -p /var/www/staging.example.com/logs
Set ownership for the web user. On Ubuntu with PHP-FPM, that’s usually www-data.
If you deploy as a different user, adjust the owner/group to match your setup.
sudo chown -R www-data:www-data /var/www/staging.example.com
sudo find /var/www/staging.example.com -type d -exec chmod 755 {} \;
sudo find /var/www/staging.example.com -type f -exec chmod 644 {} \;
Step 3: Clone WordPress files from production to staging
rsync is fast and reliable. It also lets you skip caches and other throwaway directories.
If you’re doing this mid-day on a busy site, consider enabling maintenance mode briefly. It reduces the chance of copying files mid-write.
sudo rsync -aHAX --delete \
--exclude 'wp-content/cache/' \
--exclude 'wp-content/uploads/cache/' \
/var/www/example.com/public/ \
/var/www/staging.example.com/public/
If your uploads directory is massive and you only need a quick plugin test, you can skip uploads temporarily.
For realistic WooCommerce testing, copy uploads. That keeps product images and downloads working normally.
Step 4: Export production DB and import into a staging DB
Keep staging isolated. Use a separate database and a separate user.
The examples below use MySQL/MariaDB. Swap names and passwords to match your environment.
4.1 Create staging database + user
sudo mysql -u root -p <<'SQL'
CREATE DATABASE wp_staging CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wp_staging_user'@'localhost' IDENTIFIED BY 'CHANGE_THIS_STRONG_PASSWORD';
GRANT ALL PRIVILEGES ON wp_staging.* TO 'wp_staging_user'@'localhost';
FLUSH PRIVILEGES;
SQL
4.2 Dump production database (use credentials with read access)
mysqldump -u wp_prod_user -p \
--single-transaction --quick --routines --triggers \
wp_production > /tmp/wp_production.sql
4.3 Import into staging
mysql -u wp_staging_user -p wp_staging < /tmp/wp_production.sql
rm -f /tmp/wp_production.sql
If you’re moving to a new machine (not just creating staging), use a cutover plan instead.
This guide covers the safer approach: Server Migration Tutorial (2026).
Step 5: Configure wp-config.php for staging (DB + safety flags)
Edit the staging config file:
sudo nano /var/www/staging.example.com/public/wp-config.php
Point WordPress at the staging database:
define('DB_NAME', 'wp_staging');
define('DB_USER', 'wp_staging_user');
define('DB_PASSWORD', 'CHANGE_THIS_STRONG_PASSWORD');
define('DB_HOST', 'localhost');
Add a couple of staging-friendly settings near the bottom:
define('WP_ENVIRONMENT_TYPE', 'staging');
define('DISALLOW_FILE_EDIT', true);
Optional but helpful: force the staging URL during first login. This helps even if the database still contains production values.
define('WP_HOME', 'https://staging.example.com');
define('WP_SITEURL', 'https://staging.example.com');
Step 6: Rewrite URLs in the staging database (correct way)
WordPress stores URLs in several places, including serialized data. A quick SQL replace can corrupt serialized values.
Use WP-CLI if you can.
6.1 Install WP-CLI (if missing)
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
6.2 Run search-replace on staging
cd /var/www/staging.example.com/public
sudo -u www-data wp search-replace 'https://example.com' 'https://staging.example.com' --all-tables --precise
sudo -u www-data wp search-replace 'http://example.com' 'https://staging.example.com' --all-tables --precise
Pitfall: if production answers on both www and non-www, replace both variants.
Otherwise you’ll keep chasing redirects.
Step 7: Add Nginx server block for staging
Create a dedicated Nginx config for staging. On Ubuntu, you’ll usually place it under /etc/nginx/sites-available.
sudo nano /etc/nginx/sites-available/staging.example.com
Here’s a minimal WordPress-friendly server block.
Change the PHP socket path if your PHP-FPM version differs:
server {
listen 80;
server_name staging.example.com;
root /var/www/staging.example.com/public;
index index.php index.html;
access_log /var/www/staging.example.com/logs/access.log;
error_log /var/www/staging.example.com/logs/error.log;
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 ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|webp)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
}
location ~ /\.ht {
deny all;
}
}
Enable the site and reload Nginx:
sudo ln -s /etc/nginx/sites-available/staging.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
If you prefer a more advanced reverse-proxy setup (separate app layer, WebSockets, caching), borrow the structure from this Nginx reverse proxy setup guide.
Step 8: Add SSL for staging with Let’s Encrypt
Install Certbot if it isn’t already present:
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
Request the certificate:
sudo certbot --nginx -d staging.example.com
When prompted, choose the option to redirect HTTP to HTTPS.
If you want a deeper SSL walkthrough (including validation failures and renewals), see: SSL Certificate Setup Guide (Tutorial) for Ubuntu VPS.
Step 9: Lock staging behind HTTP basic auth (keep it private)
Basic auth won’t stop a determined attacker. It will block casual crawlers, automated scans, and accidental client visits.
Install the helper tool:
sudo apt install -y apache2-utils
Create a password file:
sudo htpasswd -c /etc/nginx/.htpasswd-staging yourname
Edit the staging config (Certbot may have added a 443 server block in the same file):
sudo nano /etc/nginx/sites-available/staging.example.com
Inside the server { listen 443 ssl; ... } block, add:
auth_basic "Staging";
auth_basic_user_file /etc/nginx/.htpasswd-staging;
Reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
Quick diagnostic: if you hit an auth loop, auth is likely in the wrong place.
You probably put it inside a nested location while another location overrides it. Place auth at the server block level first, then refine.
Step 10: Block indexing and remove accidental public signals
Staging should stay out of search results and off public-facing paths.
- Robots: create a staging-only
robots.txtthat blocks everything.
cat > /var/www/staging.example.com/public/robots.txt <<'EOF'
User-agent: *
Disallow: /
EOF
sudo chown www-data:www-data /var/www/staging.example.com/public/robots.txt
- WordPress setting: in wp-admin → Settings → Reading, enable “Discourage search engines”.
Note: that checkbox is a hint, not a lock. Keep basic auth in place.
Step 11: Disable outbound email from staging (prevent surprises)
Staging will happily send password resets, order emails, form submissions, and plugin alerts. Disable mail before you start testing.
Option A (fast): install a plugin like “Disable Emails” on staging only.
Option B (code): add this as a staging-only mu-plugin:
sudo mkdir -p /var/www/staging.example.com/public/wp-content/mu-plugins
sudo nano /var/www/staging.example.com/public/wp-content/mu-plugins/disable-mail.php
<?php
/*
Plugin Name: Disable Mail (Staging)
*/
add_filter('wp_mail', function($args) {
$args['to'] = 'devnull@example.com';
$args['subject'] = '[STAGING BLOCKED] ' . ($args['subject'] ?? '');
$args['message'] = "Outbound email blocked on staging.";
return $args;
});
This won’t intercept every mail method a plugin might use. It does block the default WordPress mail path.
If you run a mail server on the same VPS, you can also firewall outbound SMTP from staging processes for extra safety.
If you send email from your VPS or dedicated server, keep production deliverability in good shape with: SPF/DKIM/DMARC setup guide and reverse DNS (rDNS) setup.
Step 12: Hardening checks specific to staging
Staging often gets ignored, which makes it a tempting target. Treat it like a real site and keep it maintained.
- Restrict wp-admin further: allow only your IP ranges (optional but strong).
- Turn off XML-RPC if you don’t need it.
- Rate-limit login endpoints to reduce bot noise.
If you want rate limits at the edge (cheap CPU insurance), follow this Nginx rate limiting tutorial and apply it to both production and staging server blocks.
Step 13: A repeatable “refresh staging” script (files + DB)
Staging is only useful if it matches reality. Refresh it weekly, or before major work, to keep tests honest.
Save this as /usr/local/sbin/refresh-wp-staging.sh and edit the variables.
sudo nano /usr/local/sbin/refresh-wp-staging.sh
#!/usr/bin/env bash
set -euo pipefail
PROD_ROOT="/var/www/example.com/public"
STAGE_ROOT="/var/www/staging.example.com/public"
PROD_DB="wp_production"
PROD_DB_USER="wp_prod_user"
STAGE_DB="wp_staging"
STAGE_DB_USER="wp_staging_user"
PROD_URL="https://example.com"
STAGE_URL="https://staging.example.com"
# Sync files (exclude caches)
rsync -aHAX --delete \
--exclude 'wp-content/cache/' \
"$PROD_ROOT/" "$STAGE_ROOT/"
# Refresh DB
TMP_DUMP="/tmp/wp_prod_$$.sql"
mysqldump -u "$PROD_DB_USER" -p \
--single-transaction --quick "$PROD_DB" > "$TMP_DUMP"
mysql -u "$STAGE_DB_USER" -p "$STAGE_DB" < "$TMP_DUMP"
rm -f "$TMP_DUMP"
# Rewrite URLs (WP-CLI)
cd "$STAGE_ROOT"
sudo -u www-data wp search-replace "$PROD_URL" "$STAGE_URL" --all-tables --precise
# Clear caches if you use a caching plugin
sudo -u www-data wp cache flush || true
echo "Staging refresh complete."
Make it executable:
sudo chmod 750 /usr/local/sbin/refresh-wp-staging.sh
Security note: the script prompts for DB passwords interactively. For non-interactive runs, use a locked-down ~/.my.cnf for a dedicated admin user.
Keep permissions tight.
Step 14: Testing checklist (updates, plugins, and performance)
Use staging to answer specific questions. Don’t stop at “does the homepage load.”
This runbook focuses on checks that tend to catch real breakage.
- PHP compatibility: change the PHP version on staging first, then run wp-admin and key pages.
- Plugin updates: update in small batches; watch Nginx and PHP-FPM logs for new errors.
- Theme changes: check templates at mobile widths; confirm menus and header scripts still fire.
- Checkout path (WooCommerce): place a test order using a sandbox gateway.
- Cache behavior: make sure you aren’t caching wp-admin or cart/checkout pages.
- Backups: take a snapshot/backup before major changes.
Quick diagnostics:
- Nginx errors:
/var/www/staging.example.com/logs/error.log - PHP-FPM errors:
sudo journalctl -u php8.3-fpm --since "30 min ago" - WordPress debug: set
WP_DEBUGtemporarily and tail logs (don’t leave it on permanently).
Step 15: Safely pushing changes from staging to production
“Push to production” can mean different things. Choose the smallest, safest move that matches what changed.
- Code-only (themes, custom plugins): deploy via Git or rsync just those directories.
- Database-only (settings, widgets): export specific tables, not the whole DB, to avoid overwriting new orders/posts.
- Full clone: only for low-change sites, or during a planned maintenance window.
On active sites—especially content-heavy or commerce—treat production data as the source of truth.
Promote code forward, and avoid pulling data backward.
Troubleshooting: common staging mistakes and quick fixes
- Staging redirects to production: run WP-CLI
search-replaceagain; checkWP_HOME/WP_SITEURLoverrides. - Mixed-content warnings: replace
http://staging...tohttps://staging...; ensure Certbot redirect is enabled. - 403/401 in wp-admin: basic auth can block AJAX endpoints. If needed, exempt
/wp-admin/admin-ajax.phpwith care. - Media missing: you didn’t sync uploads, or permissions are wrong. Fix ownership to
www-dataand resync. - Staging is slow: confirm PHP-FPM pool isn’t overloaded; don’t run production-grade caching disabled on a tiny VPS.
Summary: your staging workflow in 10 minutes next time
- Create/verify
staging.example.comDNS - rsync WordPress files into a separate docroot
- Dump prod DB → import into staging DB
- Update
wp-config.phpfor staging DB - WP-CLI search-replace URLs safely
- Nginx vhost + Let’s Encrypt
- Enable HTTP basic auth
- Block indexing + disable outbound mail
If you want this setup to stay stable under real traffic, a properly sized HostMyCode VPS gives you predictable resources for both production and staging—without sharing CPU and memory with other tenants.
Need a staging environment that mirrors production without babysitting the server? HostMyCode can provision a VPS tuned for WordPress and help you keep Nginx, PHP-FPM, and SSL tidy and up to date. Start with managed VPS hosting, or choose a self-managed HostMyCode VPS if you want full hands-on control.
FAQ
Should staging live on the same VPS as production?
For small to mid sites, yes. You get matching PHP, system libraries, and filesystem behavior.
If your VPS is already tight on RAM/CPU, put staging on a second VPS. That avoids resource contention during updates or scans.
Can I use the same database for staging and production?
Don’t. A shared database makes it too easy to overwrite live content, orders, or settings. Use a separate DB and user every time.
How do I keep staging private beyond basic auth?
Basic auth is a solid first layer. For tighter control, restrict /wp-admin by IP in Nginx and require VPN access for your team.
What’s the safest way to move changes from staging to production?
Move code forward (themes/custom plugins) and avoid copying the entire staging database back to production.
If you must move settings, export only the tables you changed and validate on a backup first.
Will Let’s Encrypt work for staging subdomains?
Yes, as long as staging.example.com resolves to your server and Nginx can answer the HTTP challenge.
If validation fails, check DNS propagation and confirm port 80 is reachable.