WordPress Staging and Checkout Recovery on FreeBSD

What you are building and why it matters
This tutorial shows you how to build a safe WordPress staging and checkout recovery workflow on FreeBSD. The goal is simple: keep a copy of your live site for testing, then use it to catch plugin conflicts, theme regressions, and checkout failures before customers hit them.
You will start from a fresh FreeBSD server, create a non-root admin user, install the required packages, set up nginx and PHP-FPM for WordPress, prepare a staging copy, and add a simple rollback path. These steps are written for hosting customers who need release safety, not lab-only experimentation.
If your production site runs on a Hostperl VPS, this workflow helps you stage changes before peak traffic and keeps support tickets lower during releases. For larger stores, Hostperl VPS hosting at Hostperl VPS hosting is a common fit for this setup.
For the checkout side of the workflow, this tutorial assumes WooCommerce or a similar cart plugin. The same pattern applies if you are testing login flows, coupon logic, or payment handoffs. For a planning companion, see WordPress checkout reliability before peak traffic and WordPress staging and migration planning for safer releases.
What this tutorial supports
- FreeBSD 14.x with pkg, rc.conf, pf, ZFS, and freebsd-update
- WordPress 6.x with nginx and PHP-FPM
- One live site and one staging copy on the same server
- Safe testing of plugin, theme, and checkout changes before release
FreeBSD uses rc.d services, pf for firewall rules, and a different filesystem layout from Linux. The commands here are FreeBSD-specific, so do not paste Linux package or service commands into this guide.
Connect to the server and identify the operating system
On your local computer:
ssh root@203.0.113.10203.0.113.10 is a reserved documentation address. Replace it with the real public IP assigned to your FreeBSD server.
Keep the root session open for now. In a second terminal later, you will test the non-root login before you change any SSH settings.
On the VPS as root:
freebsd-versionYou should see a FreeBSD release such as 14.2-RELEASE. That confirms the platform.
Create a non-root admin account and prepare SSH access
Do this before you touch WordPress. You want a normal administrative account for daily work, while root stays available for recovery.
On the VPS as root:
adduserCreate the user deploy when prompted. Use a strong password if you want password logins enabled temporarily, or set one now only for break-glass access. When the wizard asks for groups, add wheel. Finish the prompts, then verify the account exists.
id deployExpected output includes wheel in the group list. That group is the FreeBSD equivalent of a sudo-capable administrative group.
Now create the SSH directory and copy a key. Replace the example public key string with your own key contents.
mkdir -p /home/deploy/.ssh
cat > /home/deploy/.ssh/authorized_keys <<'EOF'
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyReplaceWithYourOwnKey deploy@laptop
EOF
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keysThis creates the SSH key file with the right permissions. The deploy account should now be able to log in with a key from your local machine.
Open a second terminal on your local computer:
ssh deploy@203.0.113.10Replace 203.0.113.10 with your server IP. If you use a non-default SSH port, add -p 2222. Confirm that the login works before you make any access hardening changes.
On the VPS as the non-root sudo-style admin user:
su - deploy
id
sudo -iYou should see the wheel group in the output, and sudo -i should give you a root shell if sudo is installed and allowed on the system. If sudo is not present, install it in the next section before you rely on it.
Update FreeBSD and install the required packages
Apply updates first. That lowers the chance of running a newer PHP stack on an outdated base system.
On the VPS as root:
freebsd-update fetch installRun it again if the tool asks for a second pass. Then install the web stack.
pkg update
pkg install -y nginx php82 php82-fpm php82-mysqli php82-curl php82-gd php82-mbstring php82-xml php82-zip php82-opcache mariadb114-server mariadb114-client wget unzip sudoThis installs nginx, PHP 8.2 with common WordPress extensions, MariaDB, and a few utilities. Version names can shift slightly over time in the FreeBSD package repository. If your mirror provides a newer PHP branch, use that branch consistently for both PHP and PHP-FPM packages.
Check the installed versions.
nginx -v
php -v
mysql --versionYou should see version output instead of command-not-found errors.
Enable and tune the services
FreeBSD services are managed through rc.conf and service. Set them now so the stack comes back after a reboot.
On the VPS as root:
sysrc nginx_enable=YES
sysrc php_fpm_enable=YES
sysrc mysql_enable=YESThese commands add the startup flags to /etc/rc.conf.
Start the database, PHP-FPM, and nginx.
service mysql-server start
service php-fpm start
service nginx startCheck status immediately after startup.
service mysql-server status
service php-fpm status
service nginx statusAll three should report as running. If one fails, inspect its log before continuing.
Harden the firewall with pf before exposing the site
Open SSH and web ports before you lock anything down. That avoids a self-inflicted outage.
On the VPS as root:
cat > /etc/pf.conf <<'EOF'
set block-policy drop
set skip on lo0
ext_if = "vtnet0"
pass in on $ext_if proto tcp to port { 22, 80, 443 }
pass out all keep state
EOF
pfctl -nf /etc/pf.confThe syntax test must succeed before you load the rules. If your network interface is not vtnet0, check it with ifconfig and replace the interface name in the file.
Enable pf and load the rules.
sysrc pf_enable=YES
service pf start
pfctl -srThe rule list should now show the inbound SSH and web permissions. If you are managing the server remotely and already use a custom SSH port, include that port here before closing the default rule path.
Secure MariaDB for WordPress
WordPress needs a database, but it should not use the default setup. Create a dedicated database and user.
On the VPS as root:
mysql_secure_installationSet a root password if prompted, remove anonymous users, disallow remote root login, and remove the test database. Then create the WordPress database.
mysql -u root -pAt the MariaDB prompt, run the following SQL exactly. Replace the password with your own strong secret.
CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'Use-A-Long-Random-Password-Here';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;This gives WordPress only the access it needs on localhost.
Download WordPress and prepare the document roots
Keep live and staging content separated. That makes rollback and testing much easier.
On the VPS as root:
mkdir -p /usr/local/www/wordpress-live
mkdir -p /usr/local/www/wordpress-stage
cd /tmp
fetch https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz
cp -R /tmp/wordpress/. /usr/local/www/wordpress-live/
cp -R /tmp/wordpress/. /usr/local/www/wordpress-stage/
chown -R www:www /usr/local/www/wordpress-live /usr/local/www/wordpress-stageThe www user is what nginx and PHP-FPM typically run as on FreeBSD.
Now create the WordPress configuration files.
cp /usr/local/www/wordpress-live/wp-config-sample.php /usr/local/www/wordpress-live/wp-config.php
cp /usr/local/www/wordpress-stage/wp-config-sample.php /usr/local/www/wordpress-stage/wp-config.phpEdit the live config first.
ee /usr/local/www/wordpress-live/wp-config.phpReplace the database values so they match the credentials you created, then save and exit. Use the same settings for staging if you want both sites to use the same database schema, or create a separate staging database if you prefer stronger isolation.
Add the authentication salts by fetching them from WordPress. Copy the output into both config files.
fetch -o - https://api.wordpress.org/secret-key/1.1/salt/The output is a block of PHP constants. Paste that into the config files where WordPress asks for salts.
Configure nginx for live and staging sites
Create separate server blocks so you can test safely without affecting customers or production traffic.
On the VPS as root:
cat > /usr/local/etc/nginx/nginx.conf <<'EOF'
user www;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server_tokens off;
include /usr/local/etc/nginx/conf.d/*.conf;
}
EOFNow create the site definitions.
mkdir -p /usr/local/etc/nginx/conf.d
cat > /usr/local/etc/nginx/conf.d/wordpress-live.conf <<'EOF'
server {
listen 80;
server_name 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_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass 127.0.0.1:9000;
}
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg)$ {
expires 30d;
access_log off;
}
}
EOF
cat > /usr/local/etc/nginx/conf.d/wordpress-stage.conf <<'EOF'
server {
listen 80;
server_name stage.example.com;
root /usr/local/www/wordpress-stage;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass 127.0.0.1:9000;
}
}
EOF
nginx -tThe syntax test must pass before reload. Replace example.com and stage.example.com with your actual domain names when you are ready. If you do not have DNS yet, you can test with local hosts-file overrides from your workstation.
Reload nginx only after the test succeeds.
service nginx reloadSet up PHP-FPM pools and WordPress permissions
PHP-FPM on FreeBSD often works well with the packaged defaults for a single-site setup. Confirm the service is listening where nginx expects it.
On the VPS as root:
grep -n '^listen' /usr/local/etc/php-fpm.d/www.conf
sockstat -4 -6 | grep php-fpmIf PHP-FPM is not listening on 127.0.0.1:9000, adjust either the pool or the nginx fastcgi_pass line so they match. Then restart PHP-FPM.
service php-fpm restartSet writable permissions only where WordPress needs them. Keep the rest of the tree owned by root and www as appropriate.
chown -R www:www /usr/local/www/wordpress-live /usr/local/www/wordpress-stage
find /usr/local/www/wordpress-live -type d -exec chmod 755 {} \;
find /usr/local/www/wordpress-live -type f -exec chmod 644 {} \;Do not make the whole tree world-writable. WordPress should write only to upload and cache paths when needed.
Finish the WordPress install in the browser
Point your browser at the live site, then the staging site. Complete the web installer on the live site first.
Use the database name wordpress, username wpuser, and the password you created earlier. When the install finishes, log in to WordPress and create a test post so you know the admin side works.
Repeat the browser install for the staging site if you chose a separate database. If both sites share one database for testing only, make sure the staging site is blocked from public indexing.
Build a safe checkout testing routine
For a store, the real risk is not the homepage. It is the payment path. Start with a staging plugin update, then test checkout behavior end to end.
Use the staging site to confirm these actions:
- Product pages render without PHP errors
- Cart updates correctly after quantity changes
- Coupon codes apply and remove correctly
- Checkout submits without timeout
- Order confirmation page loads after payment gateway return
If you use WooCommerce, place a real low-value test order or use the gateway's sandbox mode. Then compare the database order record and email notification with what the customer sees in the browser. This catches broken hooks, template conflicts, and email failures before launch.
If you want to plan the release side more carefully, the article WordPress checkout reliability before peak traffic covers the customer-facing risks that usually appear first under load.
Test rollback before you need it
A staging workflow is only useful if you can back out quickly. Take a database dump and keep one filesystem copy before each major plugin or theme change.
On the VPS as root:
mysqldump -u root -p wordpress > /root/wordpress-prechange.sql
cp -a /usr/local/www/wordpress-live /usr/local/www/wordpress-live.backupIf a release breaks the site, you have two recovery paths. Restore the database dump if the issue is content or schema related. Restore the filesystem copy if the issue is a theme or plugin file change.
To roll back the site files:
rm -rf /usr/local/www/wordpress-live
mv /usr/local/www/wordpress-live.backup /usr/local/www/wordpress-live
chown -R www:www /usr/local/www/wordpress-live
service nginx reload
service php-fpm restartIf you need the database back as well, restore it with:
mysql -u root -p wordpress < /root/wordpress-prechange.sqlTest the site immediately after the restore. Do not assume the backup worked until you load the homepage and a product page in a browser.
Optional: tighten SSH after verification
Only do this after the second-terminal login test has succeeded.
On the VPS as root:
grep -n 'PermitRootLogin\|PasswordAuthentication' /etc/ssh/sshd_configIf you want key-only access, edit /etc/ssh/sshd_config and set:
PermitRootLogin no
PasswordAuthentication noThen validate and reload SSH.
sshd -t
service sshd reloadKeep your existing SSH session open until the new login method is confirmed from a separate terminal. If you lock yourself out, use the provider console or rescue access.
Verify the full stack
Run these checks from the server first, then from your local machine.
On the VPS as root:
service nginx status
service php-fpm status
service mysql-server status
sockstat -4 -6 | egrep ':(80|443|9000|3306)\b'You should see the services running and the expected ports listening. Then confirm the site responds locally.
fetch -o - http://127.0.0.1/ | headThat should return HTML from WordPress, not a connection failure. Finally, test from your local machine.
On your local computer:
curl -I http://203.0.113.10Replace 203.0.113.10 with your server IP. A successful response should show HTTP headers, usually 200 OK or a redirect you intentionally configured.
For a functional smoke test, load the site in a browser, sign in to WordPress, create or edit a post, and open the checkout flow on the staging site. If the page opens quickly and the form submits without a PHP error, the deployment is working.
Troubleshooting the most likely failures
nginx will not start
Run:
nginx -t
cat /var/log/nginx/error.logIf the syntax test fails, fix the file and rerun it. The error log usually points to a bad directive, missing semicolon, or wrong path.
WordPress shows a database connection error
Run:
service mysql-server status
mysql -u root -p -e 'SHOW DATABASES;'If the database is missing, recreate it and confirm the credentials in wp-config.php. If MariaDB is down, start it and check the log under /var/db/mysql.
Checkout fails after a plugin change
Run:
tail -n 100 /var/log/nginx/error.log
php -m | egrep 'curl|mbstring|mysqli|xml|zip'Missing extensions or PHP warnings usually point to a plugin dependency or a PHP-FPM issue. Roll back the plugin on staging first, then repeat the change on live only after the failure is isolated.
Login works over SSH but the site is blocked
Run:
pfctl -sr
sockstat -4 -6 | grep nginxThis usually means the firewall rules are missing 80 or 443, or nginx is not listening on the expected address. Correct the rule set, then reload pf and nginx in that order.
For WordPress stores that need release safety, Hostperl can help you keep staging, rollback, and checkout testing under control on the right platform. If you need more room for traffic spikes or heavier plugins, compare Hostperl VPS hosting with dedicated capacity before your next launch.
When you are ready to move from test mode to production, use the same checklist here and keep a backup window open until the new release has been verified by real browser tests.
FAQ
Can I use one FreeBSD server for both live and staging WordPress sites?
Yes. Keep them in separate document roots and, ideally, separate databases. That gives you a clean rollback path and avoids testing changes directly on production files.
Should staging be public?
No. Protect it with basic auth, IP allowlisting, or at minimum robots blocking. A staging store should not be indexed or discoverable.
What if my payment gateway only has live credentials?
Use a sandbox gateway or disable payment capture on staging. If neither is available, test everything up to the final payment submit step, then verify order creation with a low-value real transaction in live hours.
How often should I test backups?
After every major plugin update and at least once before a busy sales period. A backup you never restored is only a file, not a recovery plan.
