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 |
Before proceeding with server configuration, verify that your subdomain resolves correctly:
This command should return your server's IP address, confirming DNS propagation.
Navigate to your bash scripts directory and create the necessary site directories:
When prompted, enter your subdomain name (e.g., sub.example.com). This script creates:
/var/www/sub.example.com/public_html/ - Document root directory/var/www/sub.example.com/tmp/ - Temporary files directoryCreate a new bash script for generating the subdomain server block configuration:
#!/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 <
server_name. Subdomains do NOT use the www prefix. For
example:
server_name example.com www.example.com;server_name sub.example.com; (no www variant)403 Forbidden response. This is
correct because the document root is empty. Once WordPress is installed, this will change to a
200 OK response.
store.example.comstore_example_comSave all generated credentials (database name, user, password, WordPress admin username/password, salts, and constants) to a text editor for reference.
Update the following in wp-config.php:
wp_abc123_)Open your browser and navigate to http://sub.example.com to complete the WordPress installation:
After successful installation, perform the following tasks:
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 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;
}
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
Create temporary directory:
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/
Log in to Cloudflare, navigate to My Profile → API Tokens, and view your Global API Key.
Add your Cloudflare credentials:
dns_cloudflare_email = "[email protected]"
dns_cloudflare_api_key = "your_api_key_here"
--dry-run first to test the installation. If
successful, run the same command without --dry-run to install the actual certificate.
If you encounter "DNS problem: NXDOMAIN looking up TXT for _acme-challenge.example.com", wait a few minutes for DNS propagation and try again.
# 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;
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 HTTP/3 support at https://http3check.net/
Or use browser DevTools:
Add above the PHP processing location block:
include /etc/nginx/includes/http_headers.conf;
Add above the PHP processing location block:
include /etc/nginx/includes/wp_security.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 ALL PRIVILEGES ON site_db.* FROM 'site_user'@'hostname';
GRANT SELECT, INSERT, UPDATE, DELETE ON site_db.* TO 'site_user'@'hostname';
FLUSH PRIVILEGES;
EXIT;
Add to wp-config.php:
/** DISABLE POST REVISIONS */
define('WP_POST_REVISIONS', false);
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');
Add to wp-config.php:
/** DISABLE WP-CRON */
define('DISABLE_WP_CRON', true);
Add system cron job:
*/15 * * * * wget -q -O - https://sub.example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1
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
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
If using WP Super Cache with dynamic caching, create and include the exclusions file:
Add to your server block:
include /etc/nginx/includes/wp_super_cache_exclusions.conf;
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');
| 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 |
| 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 |
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:
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.