📋 Overview
This comprehensive guide walks you through hosting an additional WordPress site on your NGINX server.
The process involves creating isolated PHP-FPM pools, configuring NGINX server blocks, implementing
SSL certificates, and applying extensive security hardening measures.
🏗️ Multi-Site Architecture
User Request
HTTP/HTTPS
→
NGINX
Port 80/443
→
PHP-FPM Pool
Isolated Process
→
WordPress
Site Files
→
MariaDB
Database
🔧 Bash Scripts Setup
Script Directory Creation
cd
mkdir wp_bash_scripts/
cd wp_bash_scripts/
1. Site Directories Script
nano 1.create_wp_directories.sh
Purpose: Creates the necessary directory structure for hosting a WordPress site,
including the public_html directory for web files and a tmp directory for temporary files.
2. Database Creation Script
nano 2.create_wp_database.sh
⚠️ Important: Do NOT use periods (.) in database names. Use underscores (_) instead.
This script generates random database credentials for enhanced security.
3. Admin Credentials Script
nano 3.admin_credentials.sh
Generates:
- 30-character random username
- 30-character random password
- WordPress security salts
- Recommended wp-config.php constants
4. NGINX Server Block Script
nano 4.nginx_server_block.sh
👥 User & Group Configuration
Creating isolated system users for each site is a critical security measure. This ensures that if one
site is compromised, the attacker cannot access files from other sites on the same server.
1Navigate to Home Directory
2Create New User
Replace username with your chosen username (e.g., nginx_help, site2_user, etc.)
3Configure Group Memberships
sudo usermod -a -G username www-data
sudo usermod -a -G www-data username
sudo usermod -a -G $USER username
User-Group Relationship
| User |
Primary Group |
Additional Groups |
Purpose |
| username |
username |
www-data, $USER |
Site-specific PHP-FPM process owner |
| www-data |
www-data |
username |
NGINX web server process |
4Apply Group Changes
exit
# Log back in to apply group membership changes
⚙️ PHP-FPM Pool Configuration
PHP-FPM pools provide process isolation for each WordPress site. Each pool runs under its own system
user, preventing cross-site contamination and limiting the impact of security breaches.
1Navigate to Pool Directory
cd /etc/php/8.3/fpm/pool.d/
ls
2Create Pool Configuration
sudo cp www.conf example.com.conf
sudo nano example.com.conf
3Essential Pool Settings
[example]
user = username
group = username
listen = /run/php/php8.3-fpm-example.com.sock
rlimit_files = 15000
rlimit_core = 100
4PHP Configuration Flags
php_flag[display_errors] = off
php_admin_value[error_log] = /var/log/fpm-php.example.log
php_admin_flag[log_errors] = on
5Create & Configure Log File
sudo touch /var/log/fpm-php.example.log
sudo chown POOL_USER:www-data /var/log/fpm-php.example.log
sudo chmod 660 /var/log/fpm-php.example.log
6Verify Socket Configuration
sudo grep "listen = /" example.com.conf
🌐 NGINX Server Block Setup
1Navigate to Configuration Directory
cd /etc/nginx/sites-available/
2Edit Server Block
sudo nano example.com.conf
3Update FastCGI Pass Directive
fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock;
Critical: The socket path must exactly match the listen directive in your
PHP-FPM pool configuration.
4Test & Reload NGINX
sudo nginx -t
sudo systemctl reload nginx
✓ Expected Output: "nginx: configuration file /etc/nginx/nginx.conf test is successful"
📦 WordPress Installation
1Download WordPress
cd
wget https://wordpress.org/latest.tar.gz
ls
2Extract Files
tar xf latest.tar.gz
cd wordpress/
ls
3Prepare Configuration
mv wp-config-sample.php wp-config.php
nano wp-config.php
Required wp-config.php Changes:
- ✓ Database Name (DB_NAME)
- ✓ Database User (DB_USER)
- ✓ Database Password (DB_PASSWORD)
- ✓ Authentication Unique Keys and Salts
- ✓ Table Prefix ($table_prefix)
- ✓ WP Constants (FS_METHOD, DISALLOW_FILE_EDIT, WP_AUTO_UPDATE_CORE)
4Copy Files & Set Ownership
cd
rsync -artv wordpress/ /var/www/example.com/public_html/
cd /var/www/example.com/
sudo chown -R www-data:www-data public_html/
ls -l
5Verify File Permissions
Next Step: Complete the WordPress installation by accessing your domain in a web
browser and following the on-screen installation wizard.
🔒 WordPress Security Hardening
Ownership & Permissions (Standard Setup)
cd /var/www/example.com/
sudo chown -R username:username public_html/ tmp/
sudo chmod 770 public_html/
sudo chmod 770 tmp/
Set Directory & File Permissions
sudo find /var/www/example.com/public_html/ -type d -exec chmod 770 {} \;
sudo find /var/www/example.com/public_html/ -type f -exec chmod 660 {} \;
Permission Schemes Comparison
| Permission Scheme |
Directories |
Files |
Use Case |
| Standard (Development) |
770 |
660 |
Active development, frequent updates |
| Hardened (Production) |
550 (core) / 770 (wp-content) |
440 (core) / 660 (wp-content) |
Production sites, maximum security |
PHP Disabled Functions
; DISABLED FUNCTIONS
php_admin_value[disable_functions] = shell_exec, opcache_get_configuration,
opcache_get_status, disk_total_space, diskfreespace, dl, exec, passthru, pclose, pcntl_alarm,
pcntl_exec, pcntl_fork, proc_open, proc_terminate, show_source, system
Reload PHP-FPM
sudo systemctl reload php8.3-fpm
Hardened Permissions (Production)
sudo find /var/www/example.com/public_html/ -type d -exec chmod 550 {} \;
sudo find /var/www/example.com/public_html/ -type f -exec chmod 440 {} \;
sudo find /var/www/example.com/public_html/wp-content/ -type d -exec chmod 770
{} \;
sudo find /var/www/example.com/public_html/wp-content/ -type f -exec chmod 660
{} \;
⚠️ Important: Only apply hardened permissions after completing site setup. With
hardened permissions, dashboard updates will fail. Use the pre-update and post-update scripts to manage
permissions during updates.
Open Base Directory Restrictions
cd /etc/php/8.3/fpm/pool.d/
sudo nano example.com.conf
Add Directives:
php_admin_value[upload_tmp_dir] = /var/www/example.com/tmp/
php_admin_value[sys_temp_dir] = /var/www/example.com/tmp/
php_admin_value[open_basedir] =
/var/www/example.com/public_html/:/var/www/example.com/tmp/
🔐 SSL Certificate Configuration
1Install SSL Certificate
sudo certbot certonly --webroot -w /var/www/example.com/public_html/ -d
example.com -d www.example.com
2Create Site-Specific SSL Configuration
sudo nano /etc/nginx/ssl/ssl_certs_example.com.conf
SSL Configuration Content:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
3Configure NGINX for HTTPS
sudo nano /etc/nginx/sites-available/example.com.conf
HTTP to HTTPS Redirect Server Block:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
HTTPS Server Block:
server {
listen 443 ssl;
http2 on;
listen 443 quic;
http3 on;
server_name example.com www.example.com;
include /etc/nginx/ssl/ssl_example.com.conf;
include /etc/nginx/ssl/ssl_all_sites.conf;
...
}
4Add HTTP Headers & FastCGI Parameters
include /etc/nginx/includes/http_headers.conf;
fastcgi_param HTTP_HOST $host;
5Test Configuration & Reload
sudo nginx -t
sudo systemctl reload nginx
6Verify Redirects
curl -I http://example.com
curl -I http://www.example.com
curl -I https://www.example.com
curl -I https://example.com
7Test SSL Configuration
Recommended Testing Tools:
NGINX Security Directives
include /etc/nginx/includes/wp_security.conf;
Rate Limiting Configuration
cd /etc/nginx/includes/
sudo cp rate_limiting_example.com.conf rate_limiting_example.net.conf
sudo nano rate_limiting_example.net.conf
Rate Limiting Configuration:
location = /wp-login.php {
limit_req zone=wp burst=20 nodelay;
limit_req_status 444;
include snippets/fastcgi-php.conf;
fastcgi_param HTTP_HOST $host;
fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock;
include /etc/nginx/includes/fastcgi_optimize.conf;
}
Rate Limiting Benefits:
- ✓ Protects against brute force attacks
- ✓ Limits requests to 30 per minute (1 request every 2 seconds)
- ✓ Does not interfere with legitimate traffic
- ✓ More efficient than WordPress plugins
Database Privilege Restriction
1Identify Database Credentials
sudo grep DB_NAME /var/www/example.com/public_html/wp-config.php
sudo grep DB_USER /var/www/example.com/public_html/wp-config.php
2Restrict Database Privileges
sudo mysql
REVOKE ALL PRIVILEGES ON database_name.* FROM 'username'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON database_name.* TO
'username'@'localhost';
FLUSH PRIVILEGES;
exit;
⚠️ Security Note: Only grant the minimum required privileges. Most WordPress operations
only need SELECT, INSERT, UPDATE, and DELETE permissions.
⚡ Performance Optimization
Post Revisions Control
define('WP_POST_REVISIONS', false);
WordPress Memory Limit
; In PHP pool configuration:
php_admin_value[memory_limit] = 256M
; In wp-config.php:
define('WP_MEMORY_LIMIT', '256M');
WordPress Cron Configuration
; In wp-config.php:
define('DISABLE_WP_CRON', true);
Setup System Cron:
crontab -e
*/15 * * * * wget -q -O - https://example.com/wp-cron.php?doing_wp_cron
>/dev/null 2>&1
Benefit: System cron runs more reliably than WP-Cron and doesn't depend on page visits.
OPcache Configuration
cd /etc/php/8.3/fpm/pool.d/
sudo nano example.com.conf
Development Server OPcache:
php_admin_value[opcache.memory_consumption] = 256
php_admin_value[opcache.interned_strings_buffer] = 32
php_admin_value[opcache.max_accelerated_files] = 20000
php_admin_flag[opcache.validate_timestamps] = 1
php_admin_value[opcache.revalidate_freq] = 2
php_admin_flag[opcache.validate_permission] = 1
Production Server OPcache:
php_admin_value[opcache.memory_consumption] = 256
php_admin_value[opcache.interned_strings_buffer] = 32
php_admin_value[opcache.max_accelerated_files] = 20000
php_admin_flag[opcache.validate_timestamps] = 0
php_admin_flag[opcache.validate_permission] = 1
⚠️ Production vs Development: The key difference is validate_timestamps.
Set to 0 in production for maximum performance, but changes require PHP-FPM restart.
FastCGI Caching
1Global NGINX Configuration
sudo nano /etc/nginx/nginx.conf
Add Cache Paths:
fastcgi_cache_path /var/run/SITE levels=1:2 keys_zone=NAME:100m inactive=60m;
fastcgi_cache_path /var/run/SITE2 levels=1:2 keys_zone=NAME2:100m
inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header http_500;
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
2Site-Specific Configuration
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_param HTTP_HOST $host;
fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock;
include /etc/nginx/includes/fastcgi_optimize.conf;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache NAME;
fastcgi_cache_valid 60m;
}
3Add Cache Controls
include /etc/nginx/includes/fastcgi_cache_excludes.conf;
add_header X-FastCGI-Cache $upstream_cache_status;
4Cache Purge Location
location ~ /purge(/.*) {
fastcgi_cache_purge NAME "$scheme$request_method$host$1";
}
Permission Management Scripts
5. Loosen Permissions Script
cd ~/wp_bash_scripts/
nano 5.loosen_permissions.sh
Purpose: Temporarily grants write permissions for WordPress dashboard updates. Run
before performing updates via the WordPress admin panel.
6. Tighten Permissions Script
nano 6.tighten_permissions.sh
Purpose: Restores hardened permissions after updates are complete. Removes write
permissions from core WordPress files while maintaining them for wp-content.
Using Permission Scripts
# Before updates:
cd ~/wp_bash_scripts/
sudo ./5.loosen_permissions.sh
# Perform WordPress updates via dashboard
# After updates:
sudo ./6.tighten_permissions.sh
📊 Monitoring & Diagnostics
PHP-FPM Pool Memory Usage
Purpose: Displays memory consumption for each PHP-FPM pool, helping you optimize
resource allocation and identify memory-hungry sites.
OPcache File Count Check
Purpose: Compares the number of PHP files in each site against the
opcache.max_accelerated_files setting. Ensures OPcache can handle all your PHP files.
✨ Best Practices & Recommendations
Security Checklist
| Task |
Status |
Priority |
| Isolated PHP-FPM pools per site |
✓ |
Critical |
| Unique system users per site |
✓ |
Critical |
| SSL/TLS certificates installed |
✓ |
Critical |
| HTTP to HTTPS redirects configured |
✓ |
Critical |
| Hardened file permissions applied |
✓ |
High |
| Database privileges restricted |
✓ |
High |
| PHP functions disabled |
✓ |
High |
| Open basedir restrictions configured |
✓ |
High |
| Rate limiting enabled |
✓ |
Medium |
| Security headers configured |
✓ |
Medium |
Performance Optimization Checklist
- ✓ OPcache configured and enabled
- ✓ FastCGI caching implemented
- ✓ Browser caching headers set
- ✓ HTTP/2 and HTTP/3 enabled
- ✓ WordPress cron optimized
- ✓ Post revisions controlled
- ✓ Memory limits properly configured
Maintenance Recommendations
Regular Tasks:
- 🔄 Monitor PHP-FPM pool memory usage weekly
- 🔄 Review error logs regularly
- 🔄 Test SSL certificate renewals monthly
- 🔄 Update WordPress core, themes, and plugins regularly
- 🔄 Backup databases and files frequently
- 🔄 Test disaster recovery procedures quarterly
- 🔄 Review and update security configurations annually
Troubleshooting Common Issues
Permission Denied Errors:
- Check file ownership matches PHP-FPM pool user
- Verify directory permissions (770 for standard, 550 for hardened)
- Ensure www-data is in the pool user's group
502 Bad Gateway Errors:
- Verify PHP-FPM socket path matches NGINX configuration
- Check PHP-FPM pool is running:
sudo systemctl status php8.3-fpm
- Review PHP-FPM error logs for issues
WordPress Update Failures:
- Run the loosen permissions script before updates
- Verify PHP-FPM pool user has write access
- Check for disabled PHP functions that might be required
- Run the tighten permissions script after successful updates
🎯 Conclusion
Congratulations! You have successfully configured a secure, isolated, and optimized
WordPress hosting environment on NGINX. This multi-site setup provides:
- ✓ Complete isolation between sites through PHP-FPM pools
- ✓ Enhanced security through permission hardening and PHP restrictions
- ✓ Optimal performance through OPcache and FastCGI caching
- ✓ Modern protocols (HTTP/2, HTTP/3, QUIC) for faster delivery
- ✓ Automated SSL certificate management
- ✓ Protection against brute force attacks via rate limiting
- ✓ Comprehensive monitoring and diagnostic capabilities
Next Steps:
- Complete WordPress installation via browser
- Install and configure essential WordPress plugins
- Set up automated backups
- Configure CDN if required
- Test all security measures
- Perform load testing to optimize performance settings
- Document your specific configuration for future reference