How to Set Up WordPress on FreeBSD
WordPress runs reliably on FreeBSD with NGINX, PHP-FPM, and MariaDB. This combination delivers better performance and tighter security than the typical Linux LAMP stack, thanks to FreeBSD's robust network stack, ZFS storage, and jails for isolation.
This guide walks through the complete setup from package installation to a production-ready WordPress site with SSL, Redis object caching, security hardening, and automated updates. For the underlying NGINX configuration, see the NGINX Production Setup guide.
Prerequisites
- FreeBSD 14.0 or later
- Root access
- A registered domain name pointing to your server's IP address
- SSL certificates (covered below, or see the Let's Encrypt guide)
Step 1: Install Required Packages
Install NGINX, PHP, MariaDB, and Redis in one command:
shpkg install nginx php83 php83-extensions php83-curl php83-gd \ php83-mbstring php83-mysqli php83-xml php83-zip php83-zlib \ php83-opcache php83-pecl-redis php83-intl php83-bcmath \ php83-fileinfo php83-pecl-imagick php83-exif php83-ftp \ php83-session php83-tokenizer php83-ctype php83-dom \ php83-simplexml php83-xmlreader php83-xmlwriter \ mariadb1011-server mariadb1011-client redis
Enable all services at boot:
shsysrc nginx_enable="YES" sysrc php_fpm_enable="YES" sysrc mysql_enable="YES" sysrc redis_enable="YES"
Step 2: Configure MariaDB
Start MariaDB and run the secure installation:
shservice mysql-server start mysql_secure_installation
Follow the prompts: set a root password, remove anonymous users, disable remote root login, and remove the test database.
Create the WordPress database and user:
shmysql -u root -p
sqlCREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'wp_user'@'localhost' IDENTIFIED BY 'StrongPasswordHere'; GRANT ALL PRIVILEGES ON wordpress.* TO 'wp_user'@'localhost'; FLUSH PRIVILEGES; EXIT;
MariaDB Performance Tuning
Edit /usr/local/etc/mysql/conf.d/server.cnf:
shcat > /usr/local/etc/mysql/conf.d/server.cnf << 'EOF' [mysqld] # InnoDB settings innodb_buffer_pool_size = 256M innodb_log_file_size = 64M innodb_flush_log_at_trx_commit = 2 innodb_flush_method = O_DIRECT # Query cache (useful for WordPress) query_cache_type = 1 query_cache_size = 64M query_cache_limit = 2M # Connection limits max_connections = 100 max_allowed_packet = 64M # Temporary tables tmp_table_size = 64M max_heap_table_size = 64M # Thread cache thread_cache_size = 16 # Logging slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow-query.log long_query_time = 2 EOF mkdir -p /var/log/mysql chown mysql:mysql /var/log/mysql service mysql-server restart
Adjust innodb_buffer_pool_size to approximately 50-70% of available RAM on a dedicated database server, or 25% on a shared server.
Step 3: Configure PHP-FPM
Edit the PHP-FPM pool configuration at /usr/local/etc/php-fpm.d/www.conf:
sh# Key settings to modify: sed -i '' \ -e 's/^listen = .*/listen = \/var\/run\/php-fpm.sock/' \ -e 's/^;listen.owner = .*/listen.owner = www/' \ -e 's/^;listen.group = .*/listen.group = www/' \ -e 's/^;listen.mode = .*/listen.mode = 0660/' \ -e 's/^pm.max_children = .*/pm.max_children = 20/' \ -e 's/^pm.start_servers = .*/pm.start_servers = 5/' \ -e 's/^pm.min_spare_servers = .*/pm.min_spare_servers = 3/' \ -e 's/^pm.max_spare_servers = .*/pm.max_spare_servers = 10/' \ /usr/local/etc/php-fpm.d/www.conf
Create the PHP configuration for WordPress:
shcp /usr/local/etc/php.ini-production /usr/local/etc/php.ini
Edit /usr/local/etc/php.ini with WordPress-optimized settings:
shcat > /usr/local/etc/php/ext-30-wordpress.ini << 'EOF' ; WordPress-optimized PHP settings upload_max_filesize = 64M post_max_size = 64M memory_limit = 256M max_execution_time = 300 max_input_vars = 3000 date.timezone = UTC ; OPcache settings opcache.enable = 1 opcache.memory_consumption = 128 opcache.interned_strings_buffer = 16 opcache.max_accelerated_files = 10000 opcache.revalidate_freq = 60 opcache.fast_shutdown = 1 EOF
Start PHP-FPM:
shservice php-fpm start
Step 4: Configure NGINX
Create the NGINX server block for WordPress:
shcat > /usr/local/etc/nginx/conf.d/wordpress.conf << 'NGINXEOF' server { listen 80; server_name example.com www.example.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name example.com www.example.com; root /usr/local/www/wordpress; index index.php index.html; # SSL configuration ssl_certificate /usr/local/etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /usr/local/etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Max upload size client_max_body_size 64M; # Gzip compression gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; gzip_min_length 1000; # WordPress permalink support location / { try_files $uri $uri/ /index.php?$args; } # PHP processing location ~ \.php$ { fastcgi_pass unix:/var/run/php-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; fastcgi_intercept_errors on; fastcgi_buffer_size 16k; fastcgi_buffers 4 16k; fastcgi_read_timeout 300; } # Static file caching location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 30d; add_header Cache-Control "public, immutable"; access_log off; } # Deny access to sensitive files location ~ /\.ht { deny all; } location ~ /wp-config\.php { deny all; } location ~ /xmlrpc\.php { deny all; } # Deny access to uploads directory PHP execution location ~* /wp-content/uploads/.*\.php$ { deny all; } # Deny access to wp-includes PHP files location ~* /wp-includes/.*\.php$ { deny all; } } NGINXEOF
Ensure the main NGINX config includes the conf.d directory. Edit /usr/local/etc/nginx/nginx.conf:
sh# Add inside the http {} block if not already present: # include /usr/local/etc/nginx/conf.d/*.conf;
Test and start NGINX:
shnginx -t service nginx start
Step 5: Install SSL with Let's Encrypt
If you do not already have SSL certificates, install them with certbot:
shpkg install py311-certbot py311-certbot-nginx # Obtain certificate certbot --nginx -d example.com -d www.example.com
Set up automatic renewal:
sh# Add to /etc/crontab echo '0 0 1 * * root certbot renew --quiet && service nginx reload' >> /etc/crontab
For detailed SSL configuration, see the Let's Encrypt on FreeBSD guide.
Step 6: Download and Install WordPress
shcd /tmp fetch https://wordpress.org/latest.tar.gz tar xzf latest.tar.gz mv wordpress /usr/local/www/wordpress chown -R www:www /usr/local/www/wordpress
Create the WordPress configuration file:
shcd /usr/local/www/wordpress cp wp-config-sample.php wp-config.php
Edit wp-config.php with your database credentials:
shsed -i '' \ -e "s/database_name_here/wordpress/" \ -e "s/username_here/wp_user/" \ -e "s/password_here/StrongPasswordHere/" \ /usr/local/www/wordpress/wp-config.php
Generate unique authentication keys and salts:
sh# Fetch fresh keys from the WordPress API fetch -o - https://api.wordpress.org/secret-key/1.1/salt/
Copy the output and replace the placeholder keys in wp-config.php.
Add the following lines before / That's all, stop editing! /:
php/** Redis object cache */ define('WP_REDIS_HOST', '127.0.0.1'); define('WP_REDIS_PORT', 6379); /** Security hardening */ define('DISALLOW_FILE_EDIT', true); define('WP_AUTO_UPDATE_CORE', 'minor'); /** Optimize for reverse proxy */ if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { $_SERVER['HTTPS'] = 'on'; }
Set proper file permissions:
sh# Directories: 755, Files: 644 find /usr/local/www/wordpress -type d -exec chmod 755 {} \; find /usr/local/www/wordpress -type f -exec chmod 644 {} \; # wp-config.php should be more restrictive chmod 640 /usr/local/www/wordpress/wp-config.php
Step 7: Configure Redis Object Caching
Redis dramatically reduces database queries by caching WordPress objects in memory.
Configure Redis:
shcat >> /usr/local/etc/redis.conf << 'EOF' # WordPress-specific Redis settings maxmemory 128mb maxmemory-policy allkeys-lru EOF service redis start
After completing the WordPress web installation (Step 8), install the Redis Object Cache plugin:
- Log into WordPress admin
- Go to Plugins > Add New
- Search for "Redis Object Cache" by Till Kruess
- Install and activate
- Go to Settings > Redis and click "Enable Object Cache"
Verify Redis is working:
shredis-cli info stats | grep keyspace redis-cli info memory | grep used_memory_human
Step 8: Complete the Web Installation
Open your browser and navigate to https://example.com. WordPress will present the installation wizard:
- Choose your language
- Enter the site title
- Create an admin username (never use "admin")
- Set a strong password
- Enter your email address
- Click "Install WordPress"
Step 9: Security Hardening
Disable XML-RPC
The NGINX config already blocks xmlrpc.php. This prevents brute-force amplification attacks that target this endpoint.
Limit Login Attempts
Install the "Limit Login Attempts Reloaded" plugin from the WordPress admin panel. Configure it to lock accounts after 3 failed attempts.
Set Up Fail2ban for WordPress
shpkg install py311-fail2ban sysrc fail2ban_enable="YES"
Create the WordPress jail configuration:
shcat > /usr/local/etc/fail2ban/jail.d/wordpress.conf << 'EOF' [wordpress] enabled = true filter = wordpress logpath = /var/log/nginx/access.log maxretry = 3 bantime = 3600 findtime = 600 EOF cat > /usr/local/etc/fail2ban/filter.d/wordpress.conf << 'EOF' [Definition] failregex = ^<HOST> .* "POST /wp-login.php ^<HOST> .* "POST /xmlrpc.php EOF service fail2ban start
File System Permissions
Ensure the web server user cannot modify core files:
sh# WordPress core files owned by root, readable by www chown -R root:www /usr/local/www/wordpress chmod -R 750 /usr/local/www/wordpress # Only uploads and cache directories writable by www chown -R www:www /usr/local/www/wordpress/wp-content/uploads chown -R www:www /usr/local/www/wordpress/wp-content/cache chmod -R 770 /usr/local/www/wordpress/wp-content/uploads chmod -R 770 /usr/local/www/wordpress/wp-content/cache
This prevents a compromised plugin from modifying core WordPress files.
Step 10: Performance Optimization
NGINX FastCGI Cache
Add a caching layer in NGINX to serve cached pages without hitting PHP at all.
In the http {} block of /usr/local/etc/nginx/nginx.conf:
nginxfastcgi_cache_path /var/cache/nginx/wordpress levels=1:2 keys_zone=WORDPRESS:100m inactive=60m max_size=512m; fastcgi_cache_key "$scheme$request_method$host$request_uri";
In the server block, add caching directives:
nginx# Skip cache for logged-in users and POST requests set $skip_cache 0; if ($request_method = POST) { set $skip_cache 1; } if ($http_cookie ~* "wordpress_logged_in") { set $skip_cache 1; } if ($http_cookie ~* "comment_author") { set $skip_cache 1; } location ~ \.php$ { fastcgi_cache WORDPRESS; fastcgi_cache_valid 200 60m; fastcgi_cache_bypass $skip_cache; fastcgi_no_cache $skip_cache; add_header X-Cache-Status $upstream_cache_status; # ... existing fastcgi settings ... }
Create the cache directory:
shmkdir -p /var/cache/nginx/wordpress chown www:www /var/cache/nginx/wordpress service nginx reload
WP-Cron via System Cron
Disable WP-Cron's default behavior (runs on every page load) and use system cron instead:
Add to wp-config.php:
phpdefine('DISABLE_WP_CRON', true);
Add to system crontab:
shecho '*/5 * * * * www /usr/local/bin/php /usr/local/www/wordpress/wp-cron.php > /dev/null 2>&1' >> /etc/crontab
Step 11: Automated Backups
Create a backup script:
shcat > /usr/local/bin/wp-backup.sh << 'BACKUPEOF' #!/bin/sh # WordPress backup script for FreeBSD BACKUP_DIR="/var/backups/wordpress" DATE=$(date +%Y%m%d_%H%M%S) DB_NAME="wordpress" DB_USER="wp_user" DB_PASS="StrongPasswordHere" WP_DIR="/usr/local/www/wordpress" mkdir -p "$BACKUP_DIR" # Database backup mysqldump -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" | gzip > "$BACKUP_DIR/db_${DATE}.sql.gz" # Files backup tar czf "$BACKUP_DIR/files_${DATE}.tar.gz" -C /usr/local/www wordpress/wp-content # Remove backups older than 30 days find "$BACKUP_DIR" -name "*.gz" -mtime +30 -delete echo "Backup completed: $BACKUP_DIR/*_${DATE}*" BACKUPEOF chmod +x /usr/local/bin/wp-backup.sh # Schedule daily backup at 3 AM echo '0 3 * * * root /usr/local/bin/wp-backup.sh' >> /etc/crontab
Step 12: WordPress Updates
Automatic Core Updates
The WP_AUTO_UPDATE_CORE setting in wp-config.php (set in Step 6) handles minor version updates automatically.
Manual Updates via WP-CLI
Install WP-CLI for command-line WordPress management:
shfetch -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar chmod +x /usr/local/bin/wp
Use WP-CLI for updates:
sh# Check for updates su -m www -c '/usr/local/bin/php /usr/local/bin/wp core check-update --path=/usr/local/www/wordpress' # Update WordPress core su -m www -c '/usr/local/bin/php /usr/local/bin/wp core update --path=/usr/local/www/wordpress' # Update all plugins su -m www -c '/usr/local/bin/php /usr/local/bin/wp plugin update --all --path=/usr/local/www/wordpress' # Update all themes su -m www -c '/usr/local/bin/php /usr/local/bin/wp theme update --all --path=/usr/local/www/wordpress'
FAQ
Can WordPress run inside a FreeBSD jail?
Yes, and it should for production deployments. Running WordPress in a jail isolates it from the host system and other services. Create a jail with VNET networking, install the same packages inside the jail, and follow this guide within the jail environment. ZFS cloning makes jail creation fast.
Which PHP version should I use?
Use the latest stable PHP branch available in the FreeBSD ports tree. As of this writing, PHP 8.3 is the recommended version. WordPress officially supports PHP 7.4+, but PHP 8.3 provides significant performance improvements, especially with JIT compilation.
How do I migrate an existing WordPress site to FreeBSD?
Export the database with mysqldump on the source server, copy the wp-content directory, import the database on FreeBSD with mysql < dump.sql, and update wp-config.php with the new database credentials. Update DNS to point to the FreeBSD server. Use WP-CLI's search-replace command if the domain changes: wp search-replace 'old-domain.com' 'new-domain.com'.
Why NGINX instead of Apache?
NGINX uses less memory, handles more concurrent connections, and serves static files faster than Apache. WordPress with NGINX and FastCGI caching typically handles 5-10x more concurrent users on the same hardware compared to Apache with mod_php.
How much RAM does WordPress on FreeBSD need?
A basic WordPress site runs on 1 GB RAM. For production with Redis, MariaDB, and NGINX caching, allocate at least 2 GB. For sites with 50K+ monthly visitors, 4 GB or more is recommended. The MariaDB InnoDB buffer pool is usually the largest memory consumer.
How do I troubleshoot a white screen of death?
Enable WordPress debug mode by adding define('WP_DEBUG', true); and define('WP_DEBUG_LOG', true); to wp-config.php. Check /usr/local/www/wordpress/wp-content/debug.log for PHP errors. Also check /var/log/php-fpm.log and the NGINX error log at /var/log/nginx/error.log. The most common cause is a PHP memory limit that is too low or a plugin conflict.
Can I use PostgreSQL instead of MariaDB?
WordPress does not natively support PostgreSQL. The PG4WP plugin exists but is not production-ready and breaks many plugins. Use MariaDB or MySQL for WordPress. If you need PostgreSQL for other applications on the same server, see the PostgreSQL on FreeBSD guide.