🚀 WordPress Multi-Site Hosting Guide

Complete Server Configuration for Subdomain WordPress Installation with NGINX, PHP-FPM & MariaDB

1. Overview & Resource Planning

💡 Important: This guide covers hosting an additional WordPress site as a subdomain on your existing NGINX server. Proper resource allocation is critical for optimal performance.

Server Resource Requirements

Site Type Minimum RAM Recommended RAM Notes
Static WordPress Site 1 GB 2 GB Minimal dynamic content
WooCommerce Site 2 GB 4 GB Heavy dynamic content
3 Sites (2 Static + 1 WooCommerce) 4 GB 8 GB Optimal performance

Multi-Site Server Architecture

NGINX
Reverse Proxy
PHP-FPM
Pool 1
WordPress
Site 1
NGINX
Reverse Proxy
PHP-FPM
Pool 2
WordPress
Site 2
NGINX
Reverse Proxy
PHP-FPM
Pool 3
WordPress
Subdomain

Initial Resource Check

Before proceeding, verify your server's current resource usage:

cd
mkdir wp_bash_scripts/
cd wp_bash_scripts/

2. Bash Scripts Setup

Creating the Subdomain Server Block Script

This bash script automates the creation of NGINX server block configuration for your subdomain:

nano 3.sub_domain_server_block
#!/bin/bash # Function to create directories and files if they don't exist create_files_and_directories() { # Check if the includes directory exists, if not create it if [ ! -d "/etc/nginx/includes" ]; then sudo mkdir -p /etc/nginx/includes fi # Check if fastcgi_optimize.conf exists, if not create it if [ ! -f "/etc/nginx/includes/fastcgi_optimize.conf" ]; then sudo tee /etc/nginx/includes/fastcgi_optimize.conf > /dev/null <<EOF fastcgi_connect_timeout 60; fastcgi_send_timeout 180; fastcgi_read_timeout 180; fastcgi_buffer_size 512k; fastcgi_buffers 512 16k; fastcgi_busy_buffers_size 1m; fastcgi_temp_file_write_size 4m; fastcgi_max_temp_file_size 4m; fastcgi_intercept_errors on; EOF fi # Check if browser_caching.conf exists, if not create it if [ ! -f "/etc/nginx/includes/browser_caching.conf" ]; then sudo tee /etc/nginx/includes/browser_caching.conf > /dev/null <<EOF location ~* \.(webp|3gp|gif|jpg|jpeg|png|ico|wmv|avi|asf|asx|mpg|mpeg|mp4|pls|mp3|mid|wav|swf|flv|exe|zip|tar|rar|gz|tgz|bz2|uha|7z|doc|docx|xls|xlsx|pdf|iso)$ { add_header Cache-Control "public, no-transform"; access_log off; expires 365d; } location ~* \.(js)$ { add_header Cache-Control "public, no-transform"; access_log off; expires 30d; } location ~* \.(css)$ { add_header Cache-Control "public, no-transform"; access_log off; expires 30d; } location ~* \.(eot|svg|ttf|woff|woff2)$ { add_header Cache-Control "public, no-transform"; access_log off; expires 30d; } EOF fi } # Function to create subdomain.domain.com.conf file create_subdomain_conf_file() { subdomain=$1 filename="/etc/nginx/sites-available/$subdomain.conf" # Define server_name for subdomain without www prefix server_name="$subdomain" # Check if the subdomain.domain.com.conf file exists, if not create it if [ ! -f "$filename" ]; then sudo tee "$filename" > /dev/null <<EOF server { listen 80; server_name $server_name; root /var/www/$subdomain/public_html; index index.php; location / { try_files \$uri \$uri/ /index.php\$is_args\$args; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm.sock; include /etc/nginx/includes/fastcgi_optimize.conf; } include /etc/nginx/includes/browser_caching.conf; access_log /var/log/nginx/access_$subdomain.log combined buffer=256k flush=60m; error_log /var/log/nginx/error_$subdomain.log; } EOF fi } # Main script starts here # Ask for subdomain name read -p "Enter subdomain name: " subdomain_name # Call function to create directories and files create_files_and_directories # Call function to create subdomain.domain.com.conf file create_subdomain_conf_file "$subdomain_name" # Display success message echo "Subdomain configuration files created successfully."
✅ Script Features:
  • Automatically creates necessary NGINX include directories
  • Generates FastCGI optimization configuration
  • Sets up browser caching rules
  • Creates subdomain-specific server block
  • Configures logging for the subdomain

3. Subdomain Server Block Configuration

Understanding the Server Block Structure

The server block defines how NGINX handles requests for your subdomain. Key components include:

  • Listen Directive: Port 80 for HTTP (initially)
  • Server Name: Your subdomain (e.g., sub.example.com)
  • Root Directory: Document root path
  • PHP Processing: FastCGI configuration
  • Logging: Access and error log paths
📝 Note: The initial configuration uses HTTP (port 80). We'll upgrade to HTTPS with SSL certificates in later steps.

4. WordPress Installation

Database Preparation

Create a dedicated database and user for your subdomain WordPress installation using bash scripts (as per your first and second site installations):

  1. Create Database
  2. Generate Admin User and Password
  3. Generate SALTS
  4. Add WP constants to text editor

Download and Configure WordPress

cd
wget https://wordpress.org/latest.tar.gz
ls
tar xf latest.tar.gz
ls

Rename and edit wp-config.php:

nano wp-config.php
⚠️ Configuration Changes Required:
  • Database Name
  • Database User
  • Database Password
  • Authentication Salts
  • Table Prefix
  • WordPress Constants

Deploy WordPress Files

Copy WordPress files to the document root and set proper ownership:

cd
sudo rsync -artv wordpress/ /var/www/sub.example.com/public_html/
cd /var/www/sub.example.com/
sudo chown -R www-data:www-data public_html/
ls -l
cd public_html/
ls -l
✅ Next Step: Complete the WordPress installation by accessing your subdomain in a web browser and following the installation wizard.

5. WordPress Hardening

PHP-FPM Pool Isolation

Creating dedicated PHP-FPM pools for each site enhances security by isolating processes:

Create Dedicated System User

sudo useradd username
sudo usermod -a -G username www-data
sudo usermod -a -G www-data username

Create PHP-FPM Pool Configuration

cd /etc/php/8.3/fpm/pool.d/
ls
sudo cp www.conf sub.example.com.conf
ls
sudo nano sub.example.com.conf
; pool name ('www' here) [sub.example] ; Default Values: The user is set to master process running user by default. ; If the group is not set, the user's group is used. user = username group = username ; Note: This value is mandatory. listen = /run/php/php8.3-fpm-sub.example.com.sock ; Set open file descriptor rlimit. rlimit_files = 15000 ; Set max core size rlimit. rlimit_core = 100 php_flag[display_errors] = off php_admin_value[error_log] = /var/log/fpm-php.www.log php_admin_flag[log_errors] = on

Update NGINX Socket Reference

sudo grep "listen = /" sub.example.conf
sudo nano /etc/nginx/sites-available/sub.example.com.conf

Update the fastcgi_pass directive:

fastcgi_pass unix:/run/php/php8.3-fpm-sub.example.com.sock;

File Ownership and Permissions

Standard Permissions (Development)

cd /var/www/sub.example.com/
sudo chown -R username:username public_html/
sudo find /var/www/sub.example.com/public_html/ -type d -exec chmod 770 {} \;
sudo find /var/www/sub.example.com/public_html/ -type f -exec chmod 660 {} \;

Hardened Permissions (Production)

sudo find /var/www/sub.example.com/public_html/ -type d -exec chmod 550 {} \;
sudo find /var/www/sub.example.com/public_html/ -type f -exec chmod 440 {} \;
sudo find /var/www/sub.example.com/public_html/wp-content/ -type d -exec chmod 770 {} \;
sudo find /var/www/sub.example.com/public_html/wp-content/ -type f -exec chmod 660 {} \;

Permission Structure

Directory/File Development Production
Directories 770 550
Files 660 440
wp-content/ dirs 770 770
wp-content/ files 660 660

PHP Function Restrictions

Disable potentially dangerous PHP functions in your pool configuration:

; ENABLED FUNCTIONS ; disk_free_space ; 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, pcntl_get_last_error, pcntl_getpriority, pcntl_setpriority, pcntl_signal, pcntl_signal_dispatch, pcntl_sigprocmask, pcntl_sigtimedwait, pcntl_sigwaitinfo, pcntl_strerror, pcntl_waitpid, pcntl_wait, pcntl_wexitstatus, pcntl_wifcontinued, pcntl_wifexited, pcntl_wifsignaled, pcntl_wifstopped, pcntl_wstopsig, pcntl_wtermsig, popen, posix_getpwuid, posix_kill, posix_mkfifo, posix_setpgid, posix_setsid, posix_setuid, posix_uname, proc_close, proc_get_status, proc_nice, proc_open, proc_terminate, show_source, system

Open_basedir Configuration

Restrict PHP file access to specific directories:

cd /var/www/sub.example.com/
ls -l
sudo mkdir tmp/
sudo chown username:username tmp/
sudo chmod 770 tmp/
ls -l

Add these directives to your PHP pool configuration:

php_admin_value[upload_tmp_dir] = /var/www/sub.example.com/tmp/ php_admin_value[sys_temp_dir] = /var/www/sub.example.com/tmp/ php_admin_value[open_basedir] = /var/www/sub.example.com/public_html/:/var/www/sub.example.com/tmp/

6. Wildcard SSL Certificate Installation

Cloudflare API Configuration

Wildcard SSL certificates allow you to secure your main domain and all subdomains with a single certificate.

📋 Prerequisites:
  • Cloudflare account with your domain
  • Global API Key from Cloudflare
  • Certbot with DNS plugin installed

Create Secure Credentials Directory

sudo mkdir /root/.cf_credentials/
sudo nano /root/.cf_credentials/cf

Add your Cloudflare credentials:

dns_cloudflare_email = "[email protected]" dns_cloudflare_api_key = "your_global_api_key_here"

Secure the Credentials

sudo chmod 400 /root/.cf_credentials/cf
sudo chmod 700 /root/.cf_credentials/

Install Wildcard Certificate

⚠️ Important: Always test with --dry-run first to avoid hitting Let's Encrypt rate limits!

Test Certificate Installation (Dry Run)

sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials /root/.cf_credentials/cf -d *.example.com --preferred-challenges dns-01 --dry-run

Install Certificate (Production)

sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials /root/.cf_credentials/cf -d *.example.com --preferred-challenges dns-01
🔧 Troubleshooting: If you encounter DNS errors (NXDOMAIN), wait a few minutes for DNS propagation and try again.

Verify Certificate Installation

sudo certbot certificates

Configure SSL for Subdomain

Create an SSL include file for your subdomain:

sudo nano /etc/nginx/ssl/ssl_sub.example.com.conf
# This include file can be used for ALL sub domains, it's *.domain # Include this file in your sites sub domain secure server block ssl_certificate /etc/letsencrypt/live/example.com-0001/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com-0001/privkey.pem; # SSL STAPLING ssl_trusted_certificate /etc/letsencrypt/live/example.com-0001/chain.pem;

Update NGINX Server Block for HTTPS

server { listen 80; # NO www.sub.domain.com server_name sub.example.com; # ADD REDIRECT TO HTTPS: 301 PERMANENT 302 TEMPORARY return 301 https://sub.example.com$request_uri; } # SECURE SERVER BLOCK server { # SECOND SERVER - NO REUSEPORT listen 443 ssl; http2 on; listen 443 quic; http3 on; server_name sub.example.com; root /var/www/sub.example.com/public_html; index index.php; # ADD SITE AND ALL SSL INCLUDE FILES include /etc/nginx/ssl/ssl_sub.example.com.conf; include /etc/nginx/ssl/ssl_all_sites.conf; location / { try_files $uri $uri/ /index.php$is_args$args; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_param HTTP_HOST $host; fastcgi_pass unix:/run/php/php8.3-fpm-sub.example.sock; include /etc/nginx/includes/fastcgi_optimize.conf; } include /etc/nginx/includes/browser_caching.conf; access_log /var/log/nginx/access_sub.example.com.log combined buffer=256k flush=60m; error_log /var/log/nginx/error_sub.example.com.log; }

Test and Reload NGINX

sudo nginx -t
sudo systemctl reload nginx

Verify HTTPS Configuration

curl -I http://sub.example.com
curl -I https://sub.example.com
✅ SSL Testing:
  • Test your certificate at SSL Labs - aim for A+ rating
  • Verify HTTP/3 support at HTTP/3 Check
  • Use browser DevTools → Network tab to confirm h3 protocol

HTTP/3 Testing (Optional)

To test HTTP/3, temporarily add this directive to prevent browser caching:

# TESTING HTTP3 AND QUIC - add to location / context to test # PREVENTS BROWSER CACHING SITE - USED TO TEST HTTP3 # COMMENT OR REMOVE DIRECTIVE AFTER CONFIRMING HTTP3 IS ENABLED add_header Cache-Control 'no-cache,no-store';
⚠️ Important: Remove this directive after confirming HTTP/3 is working properly!

7. WordPress Optimization

Disable Post Revisions

Add to wp-config.php to prevent database bloat:

/** DISABLE POST REVISIONS */ define('WP_POST_REVISIONS', false);

Increase WordPress Memory Limit

Uncomment and modify the PHP pool memory limit:

;php_admin_value[memory_limit] = 32M

Change to:

php_admin_value[memory_limit] = 256M

Add to wp-config.php:

/** MEMORY LIMIT */ define('WP_MEMORY_LIMIT', '256M');

Disable WordPress Cron

Disable WP-Cron and use system cron for better performance:

/** DISABLE WP-CRON */ define('DISABLE_WP_CRON', true);

Setup system cron job:

crontab -e

Add this line to run every 15 minutes:

*/15 * * * * wget -q -O - https://sub.example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

OPcache Configuration

Development Server Settings

; OPCACHE CONFIGURATION - DEVELOPMENT SERVER - Jul 2025 ; Directive php_admin_flag[opcache.enabled] leave commented - enabled by default ; php_admin_flag[opcache.enabled] = 1 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 Settings

; OPCACHE CONFIGURATION - PRODUCTION SERVER - Jul 2025 ; Directive php_admin_flag[opcache.enabled] leave commented - enabled by default ;php_admin_flag[opcache.enabled] = 1 ;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_value[opcache.validate_timestamps] = 0 ;php_admin_flag[opcache.validate_permission] = 1
📝 OPcache Differences:
  • Development: validate_timestamps = 1, revalidate_freq = 2 (checks for file changes)
  • Production: validate_timestamps = 0 (maximum performance, requires cache clear on code changes)

Rate Limiting

Create rate limiting configuration for your subdomain:

cd /etc/nginx/includes/
sudo cp rate_limiting_example.com.conf rate_limiting_sub.example.com.conf
sudo nano rate_limiting_sub.example.com.conf
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-sub.example.com.sock; include /etc/nginx/includes/fastcgi_optimize.conf; } location = /xmlrpc.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-sub.example.com.sock; include /etc/nginx/includes/fastcgi_optimize.conf; }

Include in your server block:

# Rate Limiting Include include /etc/nginx/includes/rate_limiting_sub.example.com.conf;

Database Privileges (Security Hardening)

Restrict database user privileges to minimum required:

REVOKE ALL PRIVILEGES ON site_db.* FROM 'site_user'@'hostname'; GRANT SELECT, INSERT, UPDATE, DELETE ON site_db.* TO 'site_user'@'hostname'; FLUSH PRIVILEGES; exit

8. PHP-FPM Memory Tuning & Server Resource Management

🚨 Critical: After adding any site, WordPress updates, new themes, or plugins, you MUST retune PHP-FPM. This is especially critical for low-resource servers (under 4GB RAM).

Understanding Memory Requirements

Memory Allocation Strategy

Server RAM Free Memory (90%) Recommended Allocation
1 GB ~900 MB Insufficient for 3 sites
2 GB ~1800 MB 2 static sites only
4 GB ~3600 MB 2 static + 1 WooCommerce
8 GB ~7200 MB Optimal multi-site setup

Memory Tuning Process

Step 1: Run Pool Memory Script

cd ~/server_bash_scripts/
sudo ./pool_memory

The script will display:

  • Pool names for all sites
  • Average memory usage per child process
  • Current process manager configuration
💡 Example Output:
  • expert_wp: 66 MB per process
  • nginx_help: 66 MB per process
  • store (WooCommerce): 93 MB per process

Step 2: Check Server Resources

htop

Press 'q' to quit htop. Note the available free memory.

Calculate pm.max_children Values

⚠️ Calculation Formula:
pm.max_children = (Available Memory * 0.9) / Average Process Memory

Example Calculation (4GB Server)

Free Memory: 3820 MB After 90% allocation: 3820 * 0.9 = 3438 MB Distribution: - Static Sites (2): 900 MB each = 1800 MB total - WooCommerce: 2000 MB Static Site Calculation: 900 MB / 66 MB per process = 13.6 → 13 processes WooCommerce Calculation: 2000 MB / 93 MB per process = 21.5 → 21 processes

Configure PHP-FPM Pools

For Static WordPress Sites (On-Demand Mode)

sudo nano /etc/php/8.3/fpm/pool.d/expert_wp.conf

Search for process manager mode (Ctrl+W):

; Comment out dynamic mode ; pm = dynamic ; Enable on-demand mode pm = ondemand ; Set max children based on calculation pm.max_children = 13 ; Uncomment and verify these directives pm.process_idle_timeout = 10s pm.max_requests = 500

For WooCommerce Sites

sudo nano /etc/php/8.3/fpm/pool.d/store.conf
; Enable on-demand mode pm = ondemand ; Set max children based on calculation pm.max_children = 21 ; Verify timeout and max requests pm.process_idle_timeout = 10s pm.max_requests = 500
📋 Process Manager Modes:
  • dynamic: Maintains minimum children always running (uses more memory)
  • ondemand: Spawns children only when needed (saves memory, slight latency)
  • static: Fixed number of children always running (highest memory, lowest latency)

Step 3: Reload PHP-FPM

sudo systemctl reload php8.3-fpm

Step 4: Verify Configuration

sudo ./pool_memory

With on-demand mode, you should see minimal or no active processes until requests are made.

Testing the Configuration

  1. Browse to each site's WordPress dashboard
  2. Navigate through a few pages (Settings → General → Dashboard)
  3. Run the pool memory script again to see active processes
  4. Monitor with htop to verify memory usage stays within limits

Memory Usage Flow

Request Arrives
PHP-FPM
Spawns Child
Process Request
(Uses ~66-93MB)
Idle Timeout
(10 seconds)
Kill Process
(Free Memory)

Server Upgrade Considerations

When to Upgrade

  • Frequent 502/503 errors
  • Slow page load times
  • High memory usage (consistently above 90%)
  • Unable to complete plugin installations
  • Adding WooCommerce or resource-intensive plugins

Upgrade Process Example (1GB → 4GB)

💡 Most Cloud Providers: Allow memory/CPU upgrades without downtime. However, downgrades are typically not supported.

After upgrading server resources:

  1. Verify new resources:
    htop
  2. Recalculate PHP-FPM values:
    sudo ./pool_memory
  3. Update MariaDB configuration:
    sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf
    # For 4GB server: innodb_buffer_pool_size = 3200M # 80% of 4GB innodb_log_file_size = 800M # 25% of buffer pool
  4. Update swap size if needed:
    Server RAM Recommended Swap
    1 GB 2 GB
    2 GB 4 GB
    4 GB 4-8 GB
    8+ GB 4-8 GB
  5. Reload all services:
    sudo systemctl reload php8.3-fpm
    sudo systemctl restart mariadb
    sudo systemctl reload nginx

Monitoring Best Practices

✅ Regular Checks:
  • Run pool_memory script weekly
  • Monitor htop for memory trends
  • Check NGINX and PHP-FPM error logs
  • Review slow query logs from MariaDB
  • Test site performance after any changes

Troubleshooting Common Issues

Issue: 502 Bad Gateway

Possible Causes:

  • PHP-FPM pool exhausted (all children busy)
  • PHP-FPM not running
  • Socket file permissions issue

Solution:

sudo systemctl status php8.3-fpm
sudo tail -f /var/log/php8.3-fpm.log

Increase pm.max_children if pool is consistently maxed out.

Issue: Slow Response Times

Possible Causes:

  • Insufficient memory allocation
  • Database not optimized
  • No caching configured

Solution:

  • Enable Redis or WP Super Cache
  • Optimize MariaDB configuration
  • Consider upgrading server resources

Issue: Out of Memory Errors

Possible Causes:

  • Too many PHP children spawned
  • Memory leaks in plugins
  • Insufficient server RAM

Solution:

dmesg | grep -i "out of memory"

Reduce pm.max_children or upgrade server RAM.

9. Caching Configuration

WP Super Cache (Dynamic Subdomain)

Create or verify the WP Super Cache exclusions file:

sudo nano /etc/nginx/includes/wp_super_cache_exclusions.conf

Include in your subdomain server block:

include /etc/nginx/includes/wp_super_cache_exclusions.conf;
sudo nginx -t
sudo systemctl reload nginx

Redis Object Cache

Configure Redis for Subdomain

Add to wp-config.php:

define( 'WP_CACHE_KEY_SALT', 'sub.example.com' ); /** PREVENT REDIS CACHING WOO SESSION DATA */ define('WP_REDIS_IGNORED_GROUPS', 'wc_session');
📝 WooCommerce Cache Exclusions:

For detailed WooCommerce caching configuration, refer to the official documentation: WooCommerce Caching Guide

Caching Strategy Comparison

Cache Type Best For Pros Cons
WP Super Cache Static sites Simple, fast, file-based Not ideal for dynamic content
Redis Dynamic/WooCommerce Memory-based, persistent, flexible Requires Redis server
OPcache All sites PHP bytecode caching, essential Code changes require cache clear
Browser Caching All sites Reduces server load, faster loads Users may see stale content

10. Final Configuration Checklist

✅ Complete Configuration Checklist

  1. Server Block Configuration
    • HTTP to HTTPS redirect configured
    • SSL certificates installed and verified
    • HTTP/2 and HTTP/3 enabled
    • Proper fastcgi_param directives in PHP location
  2. PHP-FPM Pool
    • Dedicated user created
    • Pool configuration file created
    • Socket file correctly referenced
    • Memory limits appropriately set
    • Disabled functions configured
    • OPcache configured
  3. Security Hardening
    • File permissions set (550/440 or 770/660)
    • wp-config.php secured (400)
    • open_basedir configured
    • Rate limiting enabled
    • Database privileges restricted
    • HTTP headers included
  4. WordPress Optimization
    • Post revisions disabled
    • Memory limit increased
    • WP-Cron disabled (system cron configured)
    • Caching configured (Redis or WP Super Cache)
  5. Testing & Verification
    • Site loads properly over HTTPS
    • SSL certificate A+ rating on SSL Labs
    • HTTP/3 working (verified with tools)
    • All redirects functioning correctly
    • PHP-FPM memory usage monitored
    • No errors in NGINX or PHP-FPM logs

⚠️ Post-Installation Maintenance

  • Weekly: Run pool_memory script and adjust pm.max_children if needed
  • After Updates: Clear OPcache (if using production settings)
  • Monthly: Review error logs and optimize database
  • Quarterly: Test backups and disaster recovery procedures

Summary

This comprehensive guide has covered the complete process of hosting an additional WordPress site as a subdomain on your NGINX server. Key accomplishments include:

Server Configuration Overview

Infrastructure Components:

  • ✅ NGINX web server with HTTP/2 and HTTP/3 support
  • ✅ PHP 8.3-FPM with dedicated pools per site
  • ✅ MariaDB database optimized for multi-site hosting
  • ✅ Let's Encrypt wildcard SSL certificates
  • ✅ Redis or WP Super Cache for performance

Security Measures:

  • 🔒 Process isolation through separate PHP-FPM pools
  • 🔒 Restricted file permissions and ownership
  • 🔒 Open_basedir restrictions
  • 🔒 Disabled dangerous PHP functions
  • 🔒 Rate limiting on sensitive endpoints
  • 🔒 Minimal database privileges

Performance Optimizations:

  • ⚡ OPcache for PHP bytecode caching
  • ⚡ Browser caching with appropriate headers
  • ⚡ FastCGI optimizations
  • ⚡ Disabled WordPress cron (using system cron)
  • ⚡ Object caching (Redis)
  • ⚡ Properly tuned PHP-FPM memory allocation

🎉 Congratulations!

You have successfully configured a production-ready WordPress subdomain with enterprise-level security and performance optimizations. Your server is now capable of hosting multiple WordPress sites efficiently while maintaining isolation and security between sites.