FreeBSD WordPress Hardening and Recovery Guide

Why this FreeBSD WordPress hardening guide matters
If your WordPress site runs on FreeBSD, the biggest risk usually shows up after the site is live. A plugin update breaks checkout, a theme change damages cached files, or a bad deploy leaves no clean rollback. This tutorial shows you how to harden WordPress on FreeBSD 14.x for a production site, keep recovery ready, and confirm that backups and permissions actually work.
This workflow suits store owners, agencies, and support teams that need a repeatable recovery process. If you later move the workload to a managed Hostperl VPS, the same habits still apply: isolate the app, keep secrets out of web root, and test restores before traffic depends on them. For a related cutover workflow, Hostperl also documents WordPress staging and safe cutover for real stores and WordPress rollback on AlmaLinux and Rocky Linux VPS.
What you will build
You will set up a FreeBSD WordPress host with PHP-FPM, Nginx, MariaDB, pf firewall rules, automated logical backups, a rollback snapshot point, and a simple recovery drill. The example uses 203.0.113.10 as the server IP, server.example.com as the hostname, example.com as the site domain, deploy as the non-root admin, and /opt/myapp as the application directory. Replace those values with your actual server details.
Local connection
ssh root@203.0.113.10Start from your local computer with the root SSH login above. 203.0.113.10 is a reserved documentation address, so swap it for the public IP assigned to your FreeBSD server. Leave the root session open until the new admin login works.
Confirm the OS and update FreeBSD
freebsd-versionRun this on the VPS as root. It confirms that the server is actually FreeBSD 14.x, which matters because package names and service handling are FreeBSD-specific.
freebsd-update fetch installThis applies the latest security and errata fixes. If the command reports a kernel update, finish the cycle with a reboot after the rest of the initial setup.
Create the deploy account and keep root locked down later
Do not disable root access yet. First create the new admin, verify SSH, then change the login policy only after the new account works.
adduser deployOn the VPS as root, run the interactive account creation. Use a strong password for the first login, then set up SSH keys and stop using the password.
pw groupmod wheel -m deployThis adds deploy to the wheel group so the account can use su and sudo-style administration where available.
pkg install -y sudo doasFreeBSD supports both. If you prefer sudo, keep this package; if your workflow uses doas, install both during the migration window and choose one later.
mkdir -p /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keysCopy an existing trusted key into the new account only if that key already belongs to you. If not, add the public key manually. The permissions above keep SSH from rejecting the file.
Open a second terminal on your local computer and test the new login before changing anything else.
ssh deploy@203.0.113.10If you use a non-default SSH port, include it in the command. After login, verify privilege escalation.
su -
whoami
idRun these on the VPS as the non-root sudo user. Successful output should show root after su - and wheel membership in the group list. Keep the original root session open until this works.
Install the WordPress stack on FreeBSD
This build uses Nginx, PHP-FPM, MariaDB, and a Redis cache for faster page generation. That mix works well for small stores and agency sites that need steady performance without extra kernel complexity.
pkg update
pkg install -y nginx php82 php82-fpm php82-mysqli php82-curl php82-xml php82-mbstring php82-zip php82-gd php82-opcache mariadb106-server redis wp-cliRun this on the VPS as root. If a package version differs in the FreeBSD repository, use the current php82 and mariadb106 package families available on your release.
php -v
nginx -v
mysql --versionThese checks confirm that PHP, Nginx, and MariaDB binaries are present before you write service files.
Enable services and create the application layout
sysrc nginx_enable=YES
sysrc php_fpm_enable=YES
sysrc mysql_enable=YES
sysrc redis_enable=YESThese rc.conf entries make the services start after reboot.
mkdir -p /opt/myapp /usr/local/www/nginx/example.com /var/log/nginx
chown -R deploy:deploy /opt/myappOn the VPS as root, create a clean application area outside the web root. Keeping WordPress files in /opt/myapp makes rollback and backup jobs easier.
Set up MariaDB securely
service mysql-server start
mysql_secure_installationRun the hardening wizard and set a root password, remove anonymous users, disallow remote root login, and drop the test database. Then create a dedicated database and user.
mysql -u root -pAt the MariaDB prompt, run:
CREATE DATABASE wordpress DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'Use-A-Long-Random-Secret-Here';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;Replace the sample password with a real secret. You will store it in a protected WordPress config file next.
Download WordPress and lock down file ownership
cd /opt/myapp
fetch https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz --strip-components=1
rm -f latest.tar.gz
cp wp-config-sample.php wp-config.phpRun this on the VPS as the non-root sudo user if you already changed ownership of /opt/myapp. After extraction, WordPress files live in the application directory, not in a public admin home folder.
chown -R deploy:www /opt/myapp
chmod -R u=rwX,g=rX,o= /opt/myappThis keeps the app readable by the web server but blocks world access. Adjust the web group if your Nginx or PHP-FPM user differs.
Configure WordPress for production
Edit /opt/myapp/wp-config.php and set the database credentials, salts, and cache constants.
Save the file, then restrict it further:
chmod 640 /opt/myapp/wp-config.php
chown deploy:www /opt/myapp/wp-config.phpThis prevents accidental disclosure if a backup or editor misbehaves.
Configure PHP-FPM and Redis
Check the PHP-FPM pool path for your FreeBSD package, then enable opcache and useful limits in the main PHP config. The exact file may be /usr/local/etc/php.ini and the pool config may be in /usr/local/etc/php-fpm.d/www.conf.
cp /usr/local/etc/php.ini-production /usr/local/etc/php.ini
service php-fpm restartSet the PHP-FPM socket path in the pool file, then restart after syntax checks. If you use Redis object caching later, keep the service enabled now so it is ready for plugin setup.
service redis startFor a quick Redis health check:
redis-cli pingA healthy Redis service returns PONG.
Configure Nginx for WordPress and checkout reliability
Create a dedicated server block for example.com. If you run WooCommerce, this config keeps permalinks and admin paths working cleanly.
vi /usr/local/etc/nginx/conf.d/example.com.confserver {
listen 80;
server_name example.com www.example.com;
root /opt/myapp;
index index.php index.html;
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/var/run/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 30d;
access_log off;
log_not_found off;
}
}Save and exit. Then test the syntax before restart.
nginx -t
service nginx restartA clean test should report configuration is OK. If the socket path differs, adjust fastcgi_pass to the PHP-FPM socket shown by sockstat -4 -l.
Open the firewall with pf
FreeBSD commonly uses pf on production hosts. Add rules before you close anything else, and keep SSH permitted.
vi /etc/pf.confext_if = "vtnet0"
set skip on lo
pass in on $ext_if proto tcp from any to any port { 22, 80, 443 } keep state
block allSave the file, then enable and load it carefully.
sysrc pf_enable=YES
service pf start
pfctl -nf /etc/pf.conf
pfctl -f /etc/pf.confThe syntax check must pass before the rules load. If you use a custom SSH port, add it before any reload so you do not lock yourself out.
Install WordPress, set file permissions, and run the first admin setup
Point a browser at http://example.com after DNS resolves to your FreeBSD server. Complete the WordPress installer, then immediately change ownership on the final files.
chown -R deploy:www /opt/myapp
find /opt/myapp -type d -exec chmod 750 {} \;
find /opt/myapp -type f -exec chmod 640 {} \;These permissions keep the site usable while limiting exposure if a plugin writes to disk.
Create backups and a rollback point
For WordPress recovery, you need both database and file backups. The simplest pattern on FreeBSD is a nightly SQL dump plus a filesystem snapshot if your storage is on ZFS.
mkdir -p /var/backups/wordpress
mysqldump --single-transaction --routines --triggers wordpress > /var/backups/wordpress/wordpress.sqlVerify the dump before you trust it:
test -s /var/backups/wordpress/wordpress.sql && head -n 20 /var/backups/wordpress/wordpress.sqlIf your server uses ZFS, snapshot the dataset that holds /opt/myapp and your database files. That gives you a fast rollback point before plugin changes or core updates.
Automate backup rotation with cron
crontab -eAdd this job for the root user, then save and exit:
15 2 * * * /usr/local/bin/mysqldump --single-transaction --routines --triggers wordpress > /var/backups/wordpress/wordpress-$(date +\%F).sql
20 2 * * * find /var/backups/wordpress -type f -name 'wordpress-*.sql' -mtime +7 -deleteRun a manual backup once to confirm the path and permissions work. If the file appears and is non-empty, your cron job is ready.
Test a real restore before you go live
Backups are only useful once a restore succeeds. Create a scratch database, import the dump, and confirm that tables exist.
mysql -u root -p -e "CREATE DATABASE wordpress_restore;"
mysql -u root -p wordpress_restore < /var/backups/wordpress/wordpress.sql
mysql -u root -p -e "USE wordpress_restore; SHOW TABLES;"If the tables list appears, the backup is usable. Delete the scratch database after the test.
mysql -u root -p -e "DROP DATABASE wordpress_restore;"Verification from server and client
On the VPS, confirm the services are active and listening:
service nginx status
service php-fpm status
service mysql-server status
sockstat -4 -lYou should see Nginx, PHP-FPM, MariaDB, and the expected ports or Unix sockets. Then check the site from your local computer:
curl -I http://203.0.113.10
curl -I http://example.comOne of these should return a valid WordPress response or redirect once DNS and the server block are correct. For a functional smoke test, log into WordPress, create a draft post, and load the homepage and a checkout page if WooCommerce is installed.
Troubleshooting the most likely failures
Nginx shows a 502 error. Check the PHP-FPM socket and logs.
sockstat -4 -l | grep php
tail -n 50 /var/log/nginx/example.com.error.logIf the socket path differs from /var/run/php-fpm.sock, fix the Nginx config and reload after nginx -t.
WordPress cannot connect to the database. Confirm the credentials in wp-config.php.
grep -n "DB_" /opt/myapp/wp-config.php
mysql -u wpuser -p -h 127.0.0.1 wordpress -e "SHOW TABLES;"If the database login fails, reset the MariaDB user password and update the config file.
pf blocks SSH after a rule change. Validate the file before loading it.
pfctl -nf /etc/pf.confIf syntax passes but access still fails, ensure your rule includes port 22 or your custom SSH port before the final block all.
Rollback and recovery plan
If a plugin update breaks the site, restore the previous files from your backup copy, import the last known good SQL dump, and restart the services. For ZFS-backed systems, roll back the snapshot first, then bring Nginx and PHP-FPM back online. Keep a copy of the broken state for analysis only if you need to inspect a security event.
For larger stores that need more CPU, storage IOPS, or a safer migration path, a managed managed VPS hosting plan is often easier to operate than a small self-managed box. If you also run customer portals or agency sites, Hostperl can help you plan migration windows and test restores before cutover.
Hostperl fits well when you want WordPress hosting handled like production, not a demo. If you need a VPS for a store, staging site, or recovery-ready WordPress deployment, start with Hostperl VPS hosting and keep your rollback plan close.
For teams that depend on launches, support response, and safer migrations, Hostperl’s operating model helps you move with less risk and fewer surprises.
FAQ
Can I use this setup for WooCommerce?
Yes. The Nginx, PHP-FPM, MariaDB, and backup workflow fits WooCommerce well. Just test checkout, login, and cart pages after every plugin or theme change.
Should I disable root SSH right away?
No. Verify the deploy login first, confirm privilege escalation, and only then change SSH policy.
Do I need Redis for WordPress on FreeBSD?
No, but Redis helps reduce database load on active sites. It is worth adding if your store has many repeat visits or heavy plugin traffic.
What is the fastest recovery path after a bad update?
Restore the last good database dump and site files, then clear caches and reload Nginx and PHP-FPM. If you use ZFS, rollback is even faster.
