🌐 Hosting an Additional Site - Subdomain Configuration Guide

📋 Overview: This comprehensive guide walks you through the complete process of hosting a WordPress subdomain on your NGINX server, including installation, hardening, and optimization procedures.

📑 Table of Contents

1️⃣ Prerequisites and DNS Setup

Subdomain Setup Workflow

Create DNS A Record
Point to Server IP
Create Site Directories
Configure NGINX
Install WordPress

DNS Configuration Steps

Before configuring your server, you must set up the DNS records for your subdomain. When using Cloudflare:

Field Value Description
Type A Address record type
Name sub.example.com Your subdomain name
Content Server IP Address Your server's public IP
Proxy Status DNS Only Initially set to DNS only, enable proxy later
⚠️ Important: Initially set the proxy status to "DNS Only" when setting up your site. You can enable the proxied status later after configuration is complete.

Verify DNS Resolution

Before proceeding with server configuration, verify that your subdomain resolves correctly:

ping -c 2 sub.example.com

This command should return your server's IP address, confirming DNS propagation.

2️⃣ Bash Scripts Setup

Create Bash Scripts Directory

cd
mkdir wp_bash_scripts/
cd wp_bash_scripts/

Create Site Directories Script

Navigate to your bash scripts directory and create the necessary site directories:

./4.create_site_directories.sh

When prompted, enter your subdomain name (e.g., sub.example.com). This script creates:

Verify Directory Creation

ls -l /var/www/sub.example.com/

3️⃣ Creating Subdomain Server Block

Subdomain Server Block Script

Create a new bash script for generating the subdomain server block configuration:

nano 3.sub_domain_server_block
📝 Script Contents: The subdomain server block script creates the NGINX configuration file and necessary include files automatically.
#!/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 < /dev/null < /dev/null <

Make Script Executable and Run

chmod +x 3.sub_domain_server_block
sudo ./3.sub_domain_server_block

Key Difference: Subdomain vs Domain Server Block

🔑 Important Distinction: The subdomain server block differs from a regular domain server block in one critical directive: the server_name. Subdomains do NOT use the www prefix. For example:
  • Domain: server_name example.com www.example.com;
  • Subdomain: server_name sub.example.com; (no www variant)

Enable the Configuration

sudo ln -s /etc/nginx/sites-available/sub.example.com.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Verify Server Response

curl -I http://sub.example.com
✅ Expected Result: You should receive a 403 Forbidden response. This is correct because the document root is empty. Once WordPress is installed, this will change to a 200 OK response.

4️⃣ WordPress Installation

Create Database

cd ~/wp_bash_scripts/
sudo ./5.create_site_database.sh
⚠️ Database Naming: Do NOT use periods in database names. Use underscores instead:
  • ❌ Wrong: store.example.com
  • ✅ Correct: store_example_com

Generate Admin Credentials and Salts

sudo ./6.admin_credentials.sh

Save all generated credentials (database name, user, password, WordPress admin username/password, salts, and constants) to a text editor for reference.

Download WordPress

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

Configure wp-config.php

mv wp-config-sample.php wp-config.php
nano wp-config.php

Update the following in wp-config.php:

Copy Files to Document Root

cd ~
sudo rsync -artv wordpress/ /var/www/sub.example.com/public_html/

Set Ownership

cd /var/www/sub.example.com/
sudo chown -R www-data:www-data public_html/
ls -l

Complete Installation via Browser

Open your browser and navigate to http://sub.example.com to complete the WordPress installation:

  1. Select language (English US)
  2. Enter site title
  3. Enter admin username and password
  4. Enter admin email address
  5. Click "Install WordPress"

Post-Installation Configuration

After successful installation, perform the following tasks:

5️⃣ Hardening WordPress

WordPress Hardening Components

PHP-FPM Pools
File Permissions
Open_basedir
Disabled Functions
Rate Limiting

Create Dedicated System User

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

Create Dedicated PHP-FPM Pool

cd /etc/php/8.3/fpm/pool.d/
ls
sudo cp www.conf sub.example.com.conf
sudo nano sub.example.com.conf

Update the following directives in the pool configuration:

; Pool name
[sub.example]

; Unix user/group of processes
user = username
group = username

; The address on which to accept FastCGI requests
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 configuration
php_flag[display_errors] = off
php_admin_value[error_log] = /var/log/fpm-php.www.log
php_admin_flag[log_errors] = on

Update NGINX Server Block

sudo nano /etc/nginx/sites-available/sub.example.com.conf

Update the FastCGI pass directive to use the new socket:

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm-sub.example.com.sock;
    include /etc/nginx/includes/fastcgi_optimize.conf;
}

Set File Ownership (Standard)

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 {} \;

Disable Dangerous PHP Functions

Add the following to your PHP-FPM pool configuration:

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

Hardened File Permissions

🔒 Hardened Permissions: After completing site setup and testing, apply hardened permissions:
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 {} \;

Configure open_basedir

Create temporary directory:

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

Add open_basedir directives to PHP-FPM 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 Certificates

Setup Cloudflare API Credentials

Log in to Cloudflare, navigate to My Profile → API Tokens, and view your Global API Key.

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_api_key_here"

Secure Credentials File

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

Install Wildcard Certificate (Dry Run)

sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials /root/.cf_credentials/cf -d *.example.com --preferred-challenges dns-01 --dry-run
📌 Note: Always run with --dry-run first to test the installation. If successful, run the same command without --dry-run to install the actual certificate.

Install Actual Certificate

sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials /root/.cf_credentials/cf -d *.example.com --preferred-challenges dns-01
⚠️ Common Errors:

If you encounter "DNS problem: NXDOMAIN looking up TXT for _acme-challenge.example.com", wait a few minutes for DNS propagation and try again.

Create SSL Include File

sudo nano /etc/nginx/ssl/ssl_sub.example.com.conf
# This include file can be used for ALL subdomains, it's *.domain
# Include this file in your sites subdomain 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 Server Block for HTTPS

sudo nano /etc/nginx/sites-available/sub.example.com.conf
server {
    listen 80;
    server_name sub.example.com;
    
    # Redirect to HTTPS
    return 301 https://sub.example.com$request_uri;
}

server {
    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;
    
    # Include SSL certificates
    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.com.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 Redirects

curl -I http://sub.example.com
curl -I https://sub.example.com
✅ SSL Testing: Test your SSL configuration at SSL Labs. You should receive an A+ rating.

Verify HTTP/3 Support

Test HTTP/3 support at https://http3check.net/

Or use browser DevTools:

Add HTTP Headers

sudo nano /etc/nginx/sites-available/sub.example.com.conf

Add above the PHP processing location block:

include /etc/nginx/includes/http_headers.conf;

Add Security Directives

Add above the PHP processing location block:

include /etc/nginx/includes/wp_security.conf;

Implement Rate Limiting

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 the rate limiting file in your server block:

include /etc/nginx/includes/rate_limiting_sub.example.com.conf;

Revoke Unnecessary Database Privileges

mysql -u root -p
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;

7️⃣ WordPress Optimization

Disable Post Revisions

Add to wp-config.php:

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

Increase WordPress Memory Limit

In PHP-FPM pool configuration, uncomment and modify:

php_admin_value[memory_limit] = 256M

Add to wp-config.php:

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

Configure WordPress Cron

Add to wp-config.php:

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

Add system cron job:

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

Configure OPcache (Development)

Add to PHP-FPM pool configuration for development environments:

; OPCACHE CONFIGURATION - DEVELOPMENT SERVER
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

Configure OPcache (Production)

For production environments:

; OPCACHE CONFIGURATION - PRODUCTION SERVER
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

Configure Caching Plugin (WP Super Cache)

If using WP Super Cache with dynamic caching, create and include the exclusions file:

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

Add to your server block:

include /etc/nginx/includes/wp_super_cache_exclusions.conf;

Configure Redis (Optional)

If using Redis for object caching, 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 Exclusions: For WooCommerce sites, refer to the official documentation for cache exclusions: WooCommerce Caching Configuration

Restart Services

sudo systemctl restart php8.3-fpm
sudo systemctl reload nginx

📊 Performance Testing

Recommended Testing Tools

SSL Labs
SSL Configuration
HTTP3Check
HTTP/3 Support
GTmetrix
Page Speed
Pingdom
Load Time

🔧 Troubleshooting Common Issues

Issue Possible Cause Solution
403 Forbidden after WordPress install Incorrect file ownership Run: sudo chown -R username:username public_html/
502 Bad Gateway PHP-FPM socket incorrect Verify socket path in NGINX and PHP-FPM configs match
SSL certificate errors DNS not propagated Wait 5-10 minutes and retry certificate installation
Permission denied errors Hardened permissions too restrictive Temporarily use standard permissions during setup
Plugins won't install wp-content permissions incorrect Ensure wp-content has 770 for directories, 660 for files

📋 Post-Installation Checklist

✅ Complete the Following Tasks:

  • ☐ Verify DNS resolution
  • ☐ Test HTTP to HTTPS redirect
  • ☐ Confirm SSL certificate A+ rating on SSL Labs
  • ☐ Verify HTTP/3 support
  • ☐ Test WordPress login functionality
  • ☐ Install and configure preferred theme
  • ☐ Install required plugins
  • ☐ Configure caching plugin
  • ☐ Test file upload functionality
  • ☐ Verify wp-cron is running
  • ☐ Apply hardened file permissions
  • ☐ Set up regular backups
  • ☐ Enable Cloudflare proxy (if using)
  • ☐ Perform full site functionality test

🔐 Security Best Practices Summary

Component Security Measure Benefit
PHP-FPM Dedicated pools per site Process isolation
File System Hardened permissions (550/440) Prevents unauthorized modifications
PHP Functions Disable dangerous functions Reduces attack surface
open_basedir Restrict file access Contains potential breaches
Rate Limiting Limit login/xmlrpc requests Prevents brute force attacks
Database Minimal privileges (SELECT, INSERT, UPDATE, DELETE) Limits damage from SQL injection
SSL/TLS Strong cipher suites, HTTP/3 Encrypted communications

📖 Additional Resources

💡 Final Notes

This guide provides a comprehensive approach to hosting WordPress subdomains with enterprise-grade security and performance optimizations. Always test thoroughly in a development environment before applying changes to production sites. Keep your server software, PHP version, and WordPress installation up to date for the best security and performance.

Remember: The key differences when setting up a subdomain versus a regular domain are:

  • No www variant in the server_name directive
  • Wildcard SSL certificates can be reused for multiple subdomains
  • Each subdomain should have its own dedicated PHP-FPM pool for isolation

🎉 Congratulations!

You have successfully configured a secure, optimized WordPress subdomain on your NGINX server. Your site is now protected with SSL/TLS, hardened file permissions, dedicated PHP-FPM pool, rate limiting, and performance optimizations including OPcache and caching plugins.