Back to tutorials
Tutorial

Tutorial: Set Up a Staging Server on a VPS for Safe WordPress Updates (2026 Workflow)

Tutorial: build a staging server on a VPS to test WordPress updates, plugins, and PHP changes before you touch production.

By Anurag Singh
Updated on Aug 10, 2026
Category: Tutorial
Share article
Tutorial: Set Up a Staging Server on a VPS for Safe WordPress Updates (2026 Workflow)

A broken plugin update rarely fails in a dramatic way. More often it slows pages, triggers intermittent 500s, or breaks checkout after you’ve cleared cache and moved on.

A staging server on a VPS gives you a private place to test WordPress core, plugins, themes, and PHP changes with production-like settings. You can validate updates without betting live traffic on “it should be fine.”

This walkthrough uses Ubuntu 24.04 LTS, Nginx, PHP-FPM, and Let’s Encrypt. You’ll clone production into a staging subdomain, lock it down, run updates, and then push changes to production with a repeatable process.

What you’ll build (and what you need)

You’ll set up a staging subdomain (for example, staging.example.com) either on the same VPS as production or on a separate VPS.

A separate server is the cleaner choice for high-traffic sites and agencies. Same-server staging works for many small-to-mid sites, as long as you keep it private and block outgoing email.

  • OS: Ubuntu 24.04 LTS
  • Web stack: Nginx + PHP 8.3/8.4 (match production’s major version)
  • WordPress: existing production site
  • DNS control: ability to create an A/AAAA record for the staging subdomain
  • Access: SSH as a sudo user

If you want staging isolated from production at the network level, start with a small second VPS. This is a good default for agencies and resellers.

HostMyCode’s HostMyCode VPS plans fit that setup well. You can also keep staging around as a permanent test environment.

Plan your staging layout (domain, SSL, and file paths)

Pick the staging hostname and directory layout before you copy anything. Clear naming prevents mismatched paths, wrong vhosts, and SSL confusion later.

  • Production: example.com/var/www/example.com/public
  • Staging: staging.example.com/var/www/staging.example.com/public
  • Logs: /var/log/nginx/example.com.* and /var/log/nginx/staging.example.com.*

Pro tip: Put staging on its own database.

If you ever point the wrong config at the wrong DB, you’ll catch it immediately.

Create DNS for staging (and keep TTL low)

Create an A record for staging pointing to your server IP. If you use IPv6, add an AAAA record as well.

Set a low TTL (300 seconds) while you build and validate everything.

If you’re switching DNS providers or cleaning up old records, work methodically. This guide lays out a safe sequence: DNS migration tutorial.

Provision the staging directory and permissions

Create the staging web root and use a per-site Linux user. On Ubuntu with Nginx, this keeps permissions predictable and avoids “everything is owned by www-data” sprawl.

sudo adduser --system --group --home /var/www/staging.example.com stagingwp
sudo mkdir -p /var/www/staging.example.com/public
sudo chown -R stagingwp:stagingwp /var/www/staging.example.com
sudo chmod -R 750 /var/www/staging.example.com

If you already deploy with a shared group (common for agencies), fold this into your existing model.

Don’t invent a second permissions approach unless you need it.

Install required packages (Nginx, PHP-FPM, and tooling)

Match production’s web server and PHP major version. If production is on PHP 8.3, staging should be on PHP 8.3 too.

If you don’t match, you can miss deprecations and version-specific behavior.

sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx \
  php8.3-fpm php8.3-cli php8.3-mysql php8.3-curl php8.3-xml php8.3-mbstring php8.3-zip \
  unzip rsync

If your production PHP version is different, replace 8.3 accordingly.

Create the staging database and database user

This example uses MariaDB/MySQL on the same server. If your database lives on another host, run the equivalent commands there.

sudo mysql
CREATE DATABASE wp_staging DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wp_staging_user'@'localhost' IDENTIFIED BY 'USE-A-LONG-RANDOM-PASSWORD';
GRANT ALL PRIVILEGES ON wp_staging.* TO 'wp_staging_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Store the credentials in a password manager and keep them separate from production.

Reusing the production DB user defeats the point of staging.

Copy production files to staging (rsync that won’t surprise you)

Pause edits on production for a moment. That way you don’t copy a moving target.

If the site writes constantly (for example, WooCommerce orders), take a database snapshot first. Then accept that staging will be slightly behind live.

Assuming production files are in /var/www/example.com/public:

sudo rsync -aHAX --delete \
  /var/www/example.com/public/ \
  /var/www/staging.example.com/public/

Checklist before you continue:

  • Staging has a complete copy (including wp-content and wp-config.php).
  • Ownership looks right (stagingwp owns files; Nginx/PHP can still read them).

Export and import the database (then fix URLs safely)

Dump the production DB and import it into staging. Substitute your DB names and credentials.

# Dump production
mysqldump --single-transaction --routines --triggers \
  -u root -p wp_production > /tmp/wp_production.sql

# Import into staging
mysql -u root -p wp_staging < /tmp/wp_production.sql

Next, update URLs inside the staging database.

WP-CLI is the safest tool here. It handles serialized data correctly.

If WP-CLI isn’t installed:

curl -L https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o /usr/local/bin/wp
sudo chmod +x /usr/local/bin/wp
wp --info

Run the search-replace:

cd /var/www/staging.example.com/public
sudo -u stagingwp wp search-replace 'https://example.com' 'https://staging.example.com' --all-tables
sudo -u stagingwp wp search-replace 'http://example.com' 'https://staging.example.com' --all-tables

Then set explicit values. This prevents a stray cache or plugin setting from bouncing you back to production:

sudo -u stagingwp wp option update home 'https://staging.example.com'
sudo -u stagingwp wp option update siteurl 'https://staging.example.com'

Update wp-config.php for staging (DB creds + safety flags)

Edit staging wp-config.php and point it at the staging database:

sudo nano /var/www/staging.example.com/public/wp-config.php

Set:

define('DB_NAME', 'wp_staging');
define('DB_USER', 'wp_staging_user');
define('DB_PASSWORD', 'USE-A-LONG-RANDOM-PASSWORD');

define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);

define('DISALLOW_FILE_EDIT', true);
define('AUTOMATIC_UPDATER_DISABLED', true);

Why these flags: staging should log errors to a file, not display them in the browser.

Disabling automatic updates also keeps your test runs consistent.

Prevent staging emails from reaching real customers

This staging mistake causes real damage. Password resets, order confirmations, and form submissions can go out from a test environment.

Treat email blocking as mandatory, not optional.

  • Option A (simple): Install a WordPress plugin that redirects mail to a single address (often called “disable emails” or “email logger”).
  • Option B (server-side): Block outbound SMTP from staging at the firewall, or route it to a sink SMTP service.

If production mail runs on the same VPS, be careful with firewall changes. You don’t want to break live email.

This guide helps you lock things down while keeping the right ports open: VPS firewall troubleshooting tutorial.

Lock staging behind HTTP auth (and optionally IP allowlist)

Staging shouldn’t be indexed, scraped, or casually accessed. HTTP basic auth is fast and effective.

It also keeps working even if WordPress is broken.

Create an htpasswd file:

sudo apt install -y apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd-staging stagingadmin

You’ll be prompted for a password. Use a strong one and rotate it as needed.

If you also want IP allowlisting (useful for internal teams), add allow/deny rules in the Nginx server block later.

Create an Nginx server block for staging

Create a new Nginx site config:

sudo nano /etc/nginx/sites-available/staging.example.com

Example configuration (adjust the PHP socket version if needed):

server {
  listen 80;
  server_name staging.example.com;

  root /var/www/staging.example.com/public;
  index index.php index.html;

  access_log /var/log/nginx/staging.example.com.access.log;
  error_log  /var/log/nginx/staging.example.com.error.log;

  # Keep staging private
  auth_basic "Staging";
  auth_basic_user_file /etc/nginx/.htpasswd-staging;

  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 ~* \.(?:css|js|jpg|jpeg|png|gif|ico|svg|webp|woff2?)$ {
    expires 7d;
    add_header Cache-Control "public";
  }

  location = /xmlrpc.php { deny all; }
  location ~* /wp-config.php { deny all; }
}

Enable the site and test Nginx:

sudo ln -s /etc/nginx/sites-available/staging.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Issue SSL for staging with Let’s Encrypt

Once DNS points to the right place, issue a certificate with the Nginx plugin:

sudo certbot --nginx -d staging.example.com

Choose redirect to HTTPS when prompted.

If validation or renewals fail later, this guide is a solid reference: TLS certificate renewal troubleshooting tutorial.

Disable indexing and tighten WordPress-specific staging settings

Use multiple layers. Start with auth at the web server.

Then add “don’t index this” signals for crawlers that do get access.

  • In WordPress admin: Settings → Reading → Discourage search engines from indexing
  • Add a robots.txt in staging web root:
sudo -u stagingwp tee /var/www/staging.example.com/public/robots.txt > /dev/null <<'EOF'
User-agent: *
Disallow: /
EOF

Add a staging-only visual marker so nobody edits the wrong site.

A small banner plugin or a custom admin bar note is enough.

Run your update test: core, plugins, theme, and PHP compatibility

This is where staging pays for itself. Update here first, note what changed, and test the user paths that make you money.

Recommended order:

  1. Update WordPress core
  2. Update plugins (one at a time for critical plugins)
  3. Update theme
  4. Run a quick functional test (login, forms, search, checkout)

WP-CLI keeps the process clean and repeatable:

cd /var/www/staging.example.com/public
sudo -u stagingwp wp core update
sudo -u stagingwp wp plugin update --all
sudo -u stagingwp wp theme update --all

Then apply database updates if required:

sudo -u stagingwp wp core update-db

Quick diagnostics:

  • Check WordPress debug log: /var/www/staging.example.com/public/wp-content/debug.log
  • Tail Nginx errors: sudo tail -n 80 /var/log/nginx/staging.example.com.error.log
  • Tail PHP-FPM errors (path varies): sudo journalctl -u php8.3-fpm -n 80 --no-pager

Performance sanity check: catch slow queries and heavy plugins early

You don’t need a full benchmarking suite to spot obvious regressions.

With the same caching and PHP settings, staging often reveals the plugin that added 300 ms to TTFB.

  • Measure a few pages with curl:
curl -s -o /dev/null -w 'TTFB:%{time_starttransfer} Total:%{time_total}\n' https://staging.example.com/

If you use Nginx fastcgi cache on production, mirror it on staging. That way you test real behavior.

This tutorial shows a practical microcache setup: Nginx caching tutorial for WordPress.

Promote changes to production (three safe options)

Promotion isn’t one-size-fits-all. Choose the method that matches what actually changed.

Option 1: Code-only deploy (best for plugin/theme updates)

If staging changes are mostly plugin/theme updates, push only wp-content changes and exclude uploads.

This is the lowest-risk route for most update cycles.

# From production server, pull from staging copy (same server example)
sudo rsync -aHAX --delete \
  --exclude 'uploads/' \
  /var/www/staging.example.com/public/wp-content/ \
  /var/www/example.com/public/wp-content/

Then run any required database updates on production from wp-admin (or WP-CLI).

Option 2: Full file sync + DB sync (best for major refactors)

Use this when staging includes content model changes, large theme rewrites, or you’re restoring a broken production state.

  • Put production in maintenance mode.
  • Sync files.
  • Import staging DB to production (be cautious: you may overwrite new orders/comments created since the staging snapshot).

For WooCommerce or membership sites, avoid this unless you’ve planned how to reconcile live data.

Option 3: Blue/green with a second VPS (best for agencies and busy stores)

Run staging on a second VPS, validate it, then swap DNS or reverse proxy upstreams.

Rollback becomes a simple switch, not a scramble.

If you manage a lot of client changes, consider managed VPS hosting. It keeps patching, monitoring, and baseline hardening from eating your time.

Rollback plan you can actually execute in 10 minutes

Before you deploy anything, make rollback boring. If rollback depends on “remembering what we changed,” it will fail under pressure.

  • Files: keep a timestamped tarball of wp-content from production
  • Database: take a fresh DB dump (or snapshot) right before deploying
  • Config: keep your Nginx and PHP-FPM configs in version control if possible
# Example: quick wp-content backup
sudo tar -C /var/www/example.com/public -czf /root/wp-content-$(date +%F-%H%M).tgz wp-content

# Example: quick DB dump
mysqldump --single-transaction -u root -p wp_production > /root/wp_production-$(date +%F-%H%M).sql

If you want stricter coverage (snapshots + offsite + restore tests), follow a 3-2-1 strategy.

This guide pairs well with staging workflows: VPS backup strategy tutorial.

Troubleshooting: common staging failures (and fast fixes)

Staging redirects to production

  • Run WP-CLI option get home and option get siteurl; fix them.
  • Search for hard-coded URLs in plugins or theme config.
  • Clear caches (plugin cache + Nginx cache if enabled).

Mixed content after SSL

  • Confirm your staging URL is https:// in home/siteurl.
  • Run a second WP-CLI search-replace from http:// to https://.

500 errors only on staging

  • Check /var/log/nginx/staging.example.com.error.log and PHP-FPM logs.
  • Confirm the PHP version matches production (8.3 vs 8.4 differences matter).
  • Raise PHP memory limit for staging to reproduce production behavior.

Let’s Encrypt fails on staging

  • Confirm DNS points to the right IP (A/AAAA), and port 80 is reachable.
  • Ensure firewall allows inbound 80/443.
  • Re-run certbot after fixing routing or Nginx syntax.

Operational checklist: keep staging useful (not a forgotten clone)

  • Sync cadence: refresh staging from production monthly (or before major updates).
  • Access control: keep HTTP auth on; rotate credentials when staff changes.
  • Email safety: verify emails are still blocked after plugin updates.
  • Plugin parity: keep the plugin list identical to production, unless you’re testing a replacement.
  • Resource limits: staging should mirror production PHP limits and caching rules to be meaningful.

Summary: a staging workflow that reduces real downtime

A staging environment won’t catch everything. It doesn’t need to.

It needs to catch expensive failures—broken checkout, PHP incompatibilities, and slow plugins—before they land on production.

Once this becomes habit, updates get faster and rollbacks stop being an emergency.

If you want staging physically separated from production, run it on a second VM. Keep it as a standing test box.

You can start with a HostMyCode VPS for staging, then move to dedicated servers when you need more predictable performance under load.

If you’re building staging for client sites or shipping WordPress changes every week, a VPS gives you control you won’t get on shared hosting. HostMyCode offers VPS hosting for hands-on admins and managed VPS hosting if you want patching, monitoring, and baseline hardening handled for you.

FAQ

Should staging be on the same VPS as production?

For small sites, same-VPS staging is fine if it’s protected with auth and can’t send email. For busy WooCommerce sites, a separate VPS keeps resource spikes and security boundaries cleaner.

Do I need a separate SSL certificate for staging?

Yes. Issue a certificate for staging.example.com (or use a wildcard if you already manage DNS validation). Keep staging on HTTPS so you catch mixed-content and cookie issues early.

How do I stop Google from indexing staging?

Use HTTP basic auth first (bots can’t pass it). Then set WordPress to discourage indexing and add a robots.txt that disallows all.

What’s the safest way to test plugin updates?

Update one critical plugin at a time on staging, test the related user journey (forms, payments, login), then promote code-only changes to production. Always take a backup right before promotion.

Can I use this approach for multiple client sites?

Yes. Create one staging vhost per client, use separate Linux users, and keep each staging environment behind its own auth credentials. For agencies, isolating staging on a second VPS per batch of clients often simplifies risk management.