🚀 NGINX WordPress Multi-Site Hosting Guide

Section 19: Complete Setup & Hardening Process

angelscript

📋 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

cd
clear

2Create New User

sudo useradd username
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] ; Pool name
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

cd public_html/
ls -l
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

nano pool_list_usage.sh
Purpose: Displays memory consumption for each PHP-FPM pool, helping you optimize resource allocation and identify memory-hungry sites.

OPcache File Count Check

nano opcache_files.sh
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