IPv4 & IPv6 Leasing - Any RIR, Any LocationOrder Now
Hostperl

WordPress Staging on FreeBSD with Safe Cutover

By Raman Kumar

Share:

Updated on Oct 16, 2026

WordPress Staging on FreeBSD with Safe Cutover

Why this staging workflow matters

If your WordPress site sells products, takes bookings, or supports agency clients, you need a staging copy that behaves like production. You also need a cutover path you can reverse quickly.

This tutorial shows how to build WordPress staging on FreeBSD with a separate document root, a separate database, and a controlled switch to the live site after you verify updates and checkout behavior.

You will start from a fresh FreeBSD server, create a non-root admin account, install the required packages, and deploy Nginx, PHP-FPM, and MariaDB. You will also protect staging with HTTP basic authentication so search engines and customers do not index it.

For hosting teams running production on Hostperl, this release process helps avoid late-night rollback calls and half-finished launches. If you are planning a VPS for a WordPress workload, Hostperl VPS gives you the control needed for this workflow.

What you need before you start

  • A FreeBSD 14.2-RELEASE or newer server.
  • A domain such as example.com already pointing to the server.
  • Root SSH access to the VPS.
  • A second SSH session or terminal for testing the new sudo-capable account.
  • Backups of the live WordPress files and database before you copy anything.

For a deeper look at rollback planning around application releases, Hostperl’s rollback tutorial and the WooCommerce recovery guide show the same discipline applied to different stacks.

Connect to the server and confirm FreeBSD

On your local computer

ssh root@203.0.113.10

Replace 203.0.113.10 with the public IP assigned to your FreeBSD server. Keep this root session open until the new admin login is confirmed in a second terminal.

On the VPS as root

freebsd-version

You should see a FreeBSD release string such as 14.2-RELEASE. That confirms the OS track for this tutorial.

Create a non-root admin account

FreeBSD does not use Linux groups like sudo or wheel in exactly the same way as Linux distributions, but the wheel group remains the standard path for privileged access with doas or su.

The safest pattern is to create the account, add it to wheel, and verify the login before you change any root SSH settings.

On the VPS as root

adduser

Follow the prompts and create a user named deploy. When asked for groups, include wheel.

Set a strong password if you intend to use password login temporarily, or leave password-based SSH off if you already have keys ready.

Now create a minimal doas policy so the new account can administer the server.

On the VPS as root

mkdir -p /usr/local/etc/doas.conf
cat > /usr/local/etc/doas.conf <<'EOF'
permit persist keepenv :wheel
EOF
chmod 0400 /usr/local/etc/doas.conf

This file lets users in wheel run privileged commands with doas. The permission mode matters; FreeBSD ignores a policy file if it is too open.

Next, prepare SSH access for the new account.

On the VPS as root

mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys 2>/dev/null || true
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

If root did not already have an authorized_keys file, copy your public key into /home/deploy/.ssh/authorized_keys manually from your local computer. The permissions above are required for SSH to accept the key.

On your local computer

ssh deploy@203.0.113.10

Open this in a second terminal. Log in as deploy, then test privilege escalation.

On the VPS as the non-root sudo user

doas id

The output should show uid=0(root). If it does, the admin handoff is ready.

Update packages and enable the services you need

FreeBSD uses pkg for software management and rc.conf for persistent service startup. Update package metadata first, then install Nginx, PHP-FPM, MariaDB, and the PHP extensions WordPress needs.

On the VPS as the non-root sudo user

doas pkg update
 doas pkg install -y nginx php83 php83-fpm php83-mysqli php83-curl php83-gd php83-mbstring php83-xml php83-zip php83-opcache mariadb106-server mariadb106-client wget tar unzip

Package names can change with major PHP or MariaDB updates. If your repository offers newer branches, choose the matching phpXX and mariadbXX families available on your server.

Enable the services at boot.

On the VPS as the non-root sudo user

doas sysrc nginx_enable="YES"
doas sysrc php_fpm_enable="YES"
doas sysrc mysql_enable="YES"

Start the services and confirm they are running.

On the VPS as the non-root sudo user

doas service php-fpm start
doas service mysql-server initdb
doas service mysql-server start
doas service nginx start
doas service nginx status
doas service php-fpm status
doas service mysql-server status

At this point, the services should report running. If MariaDB fails on the first start, the init database step is usually the missing piece.

Create the production and staging database pair

For a safe staging cutover, use separate databases. That lets you test imports and schema changes without touching live data until you are ready.

On the VPS as the non-root sudo user

doas mysql_secure_installation

Answer the prompts to set a root password, remove anonymous users, disable remote root login, and remove the test database. That hardens the database before WordPress touches it.

Now create two databases and one WordPress user with access to both.

On the VPS as the non-root sudo user

doas mysql -u root -p

At the MariaDB prompt, run the following SQL, then exit with quit.

CREATE DATABASE wordpress_live CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE wordpress_stage CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wpadmin'@'localhost' IDENTIFIED BY 'Use-A-Long-Random-Password-Here';
GRANT ALL PRIVILEGES ON wordpress_live.* TO 'wpadmin'@'localhost';
GRANT ALL PRIVILEGES ON wordpress_stage.* TO 'wpadmin'@'localhost';
FLUSH PRIVILEGES;

Replace the password with a unique secret. Do not reuse a production panel password or an email password.

Prepare the document roots and PHP-FPM socket

Keep the live and staging files separate. That makes rollback easier because you can switch the web root without rewriting the application itself.

On the VPS as the non-root sudo user

doas mkdir -p /usr/local/www/wordpress-live /usr/local/www/wordpress-stage /usr/local/www/example.com/public_html
 doas chown -R www:www /usr/local/www/wordpress-live /usr/local/www/wordpress-stage /usr/local/www/example.com

Create a dedicated PHP-FPM pool for WordPress if the default pool is too generic. This example uses the default pool path, which is acceptable for a single-site server.

On the VPS as the non-root sudo user

doas cp /usr/local/etc/php-fpm.d/www.conf /usr/local/etc/php-fpm.d/wordpress.conf
 doas sed -i '' 's/^;*listen = .*/listen = \/var\/run\/php-fpm-wordpress.sock/' /usr/local/etc/php-fpm.d/wordpress.conf
 doas sed -i '' 's/^user = .*/user = www/' /usr/local/etc/php-fpm.d/wordpress.conf
 doas sed -i '' 's/^group = .*/group = www/' /usr/local/etc/php-fpm.d/wordpress.conf

Verify the file if your editor or defaults differ. Some FreeBSD package builds keep the default pool enabled, so a separate pool file is optional if the standard socket already works for your setup.

Restart PHP-FPM after editing, then confirm the socket exists.

On the VPS as the non-root sudo user

doas service php-fpm restart
ls -l /var/run/php-fpm-wordpress.sock

Install WordPress files for live and staging

Download WordPress once, then copy it into the live and staging trees. That keeps the base code identical while the databases remain separate.

On the VPS as the non-root sudo user

cd /tmp
fetch https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz
cp -R wordpress/. /usr/local/www/wordpress-live/
cp -R wordpress/. /usr/local/www/wordpress-stage/
chown -R www:www /usr/local/www/wordpress-live /usr/local/www/wordpress-stage

Next, create the two configuration files. The live site should point to wordpress_live; staging should point to wordpress_stage.

On the VPS as the non-root sudo user

cd /usr/local/www/wordpress-live
cp wp-config-sample.php wp-config.php
cd /usr/local/www/wordpress-stage
cp wp-config-sample.php wp-config.php

Edit the live configuration first.

On the VPS as the non-root sudo user

cd /usr/local/www/wordpress-live
vi wp-config.php

Replace the database section with the following values, then save and exit vi with :wq.

define( 'DB_NAME', 'wordpress_live' );
define( 'DB_USER', 'wpadmin' );
define( 'DB_PASSWORD', 'Use-A-Long-Random-Password-Here' );
define( 'DB_HOST', 'localhost' );
define( 'DB_CHARSET', 'utf8mb4' );
define( 'DB_COLLATE', '' );

Repeat the same change in the staging config, but set DB_NAME to wordpress_stage. Then generate unique salts for both files from WordPress.org’s secret-key service and paste them into each configuration file.

Configure Nginx for live and staging hosts

Use two server blocks: one for the production hostname and one for staging. Put HTTP basic auth on staging so customers and search engines do not reach it accidentally.

On the VPS as the non-root sudo user

doas mkdir -p /usr/local/etc/nginx/conf.d /usr/local/etc/nginx/htpasswd

Create a password file for staging.

On the VPS as the non-root sudo user

doas sh -c 'printf "staginguser:$(openssl passwd -apr1 StrongStagingPassword)\n" > /usr/local/etc/nginx/htpasswd/example.com'

Now create the Nginx site configuration.

On the VPS as the non-root sudo user

doas vi /usr/local/etc/nginx/conf.d/example.com.conf

Paste this content, then save and exit with :wq.

server {
    listen 80;
    server_name example.com www.example.com;
    root /usr/local/www/wordpress-live;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/var/run/php-fpm-wordpress.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    location ~* \.(log|sql|bak|ini)$ {
        deny all;
    }
}

server {
    listen 80;
    server_name staging.example.com;
    root /usr/local/www/wordpress-stage;
    index index.php index.html;

    auth_basic "Staging Area";
    auth_basic_user_file /usr/local/etc/nginx/htpasswd/example.com;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/var/run/php-fpm-wordpress.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}

Test the configuration before you reload Nginx.

On the VPS as the non-root sudo user

doas nginx -t

You want to see syntax is ok and test is successful. If that passes, reload Nginx.

On the VPS as the non-root sudo user

doas service nginx reload

Open the firewall and keep the old path available

FreeBSD commonly uses pf for packet filtering. Before you enable any restrictive rule set, make sure SSH remains open. Do not cut off your own session.

On the VPS as the non-root sudo user

doas sysrc pf_enable="YES"
doas sysrc pf_rules="/etc/pf.conf"

Edit /etc/pf.conf so it allows SSH and web traffic.

On the VPS as the non-root sudo user

doas vi /etc/pf.conf

Use this starter policy and save it with :wq.

ext_if="vtnet0"
set skip on lo

block in all
pass out all keep state
pass in on $ext_if proto tcp to port { 22, 80, 443 } keep state

Test and load the firewall only after you have a second session ready.

On the VPS as the non-root sudo user

doas pfctl -nf /etc/pf.conf
doas service pf restart
doas pfctl -sr

The ruleset should list SSH, HTTP, and HTTPS access. If you are using a Hostperl deployment on a VPS that already has traffic, this is the point where you would add the new rule before removing the old one.

Test the staging site and copy the live database

Before any cutover, confirm that staging renders and that the database import works. If you already have a production database dump, import it into wordpress_stage first. That lets you test a realistic copy of your data.

On the VPS as the non-root sudo user

doas mysql -u root -p wordpress_stage < /path/to/your-production-backup.sql

Replace the dump path with the file from your backup workflow. Then browse to http://staging.example.com from your client browser and log in with the staging basic-auth credentials plus your WordPress admin account.

If the WordPress login page loads but your theme or plugins error out, check the PHP-FPM and Nginx logs.

On the VPS as the non-root sudo user

doas tail -n 50 /var/log/nginx/error.log
doas tail -n 50 /var/log/messages

Typical clues include a bad SCRIPT_FILENAME path, a wrong socket name, or a plugin expecting a missing table. Fix the file path or restore the missing database object, then retest.

Cut over from staging to live with a reversible switch

When staging is clean, update the live site. Keep a rollback copy of the old configuration and database dump before you change anything.

On the VPS as the non-root sudo user

doas cp /usr/local/www/wordpress-live/wp-config.php /usr/local/www/wordpress-live/wp-config.php.bak
 doas mysqldump -u root -p wordpress_live > /root/wordpress_live_backup.sql

If the site is simple, the cutover may be just a database import from staging or a path switch. For a content refresh, you might replace the live database with the verified staging data after a final snapshot.

On the VPS as the non-root sudo user

doas mysql -u root -p wordpress_live < /path/to/verified-staging-dump.sql

For a rollback, restore wordpress_live_backup.sql and the .bak config file, then reload Nginx and PHP-FPM.

Verification from server and client

On the VPS as the non-root sudo user

doas service nginx status
doas service php-fpm status
doas service mysql-server status
doas sockstat -4 -6 | grep -E '(:80|:443|php-fpm|mysql)'

You should see Nginx, PHP-FPM, and MariaDB running, with the web server listening on port 80. If you later add TLS, repeat the same check for port 443.

On your local computer

curl -I http://example.com
curl -I http://staging.example.com

The production host should return a normal HTTP response, and the staging host should return 401 Unauthorized until you supply the basic-auth credentials. That confirms the staging barrier is working.

Finally, reboot the server and confirm the services come back automatically.

On the VPS as the non-root sudo user

doas reboot

After the server returns, reconnect with SSH and rerun the status commands. The site should still answer on the same hostnames after the reboot.

Troubleshooting the most likely failures

1. Nginx will not start. Run doas nginx -t first. If the output points to a syntax error or an unknown directive, open the referenced file and fix the line number before reloading.

2. WordPress shows a database connection error. Check the credentials in wp-config.php and test the account with doas mysql -u wpadmin -p -e "SHOW DATABASES;". If that fails, reset the password and update the config file.

3. Staging prompts for basic auth but the browser still shows the live site. Confirm the hostname resolves to the right server with host staging.example.com from your local computer. A stale DNS record is a common cause during migrations.

4. Plugin updates break the site. Restore /usr/local/www/wordpress-live/wp-config.php.bak and your database backup, then disable the last plugin you changed inside the staging copy first. This is why the tutorial keeps staging and live separate.

Where Hostperl fits this workflow

Hostperl is a good fit when you need a WordPress-ready server you can manage directly, test safely, and roll back without waiting on a ticket queue. For smaller production sites, a managed VPS hosting plan is usually the simplest starting point. For larger stores or agencies handling multiple client cutovers, a dedicated deployment path can be the better fit.

If your team also works with content-heavy stores, the WooCommerce cart recovery article pairs well with this tutorial because it follows the same staging-first approach.

Use this staging pattern before every major WordPress plugin, theme, or PHP upgrade. Hostperl can provide the VPS foundation for that workflow, and the same cutover discipline applies whether you are launching a single site or a client portfolio. If you want a server you can control end to end, start with Hostperl VPS or scale up to a larger platform when your checkout traffic grows.

FAQ

Can I use this on a live FreeBSD WordPress server without staging?
Yes, but you should not. Staging lets you test updates and database imports before customers see them.

Why separate databases instead of copying the live database over the top?
Separate databases make rollback faster and reduce the chance of overwriting live content during a test cycle.

Do I need TLS before going live?
Yes. This tutorial keeps the focus on staging and cutover, but you should add Let’s Encrypt or another certificate before production traffic is public.

What if my theme depends on hardcoded URLs?
Search and replace the old domain inside the staging database first, then recheck menus, images, and checkout links before cutover.