How to architect an infrastructure to serve millions of blogs
WordPress.com hosts over 522 million websites and serves more than 409 million unique visitors monthly, making it one of the largest multi-tenant platforms on the internet. Edublogs, a specialized educational blogging platform, manages over 5 million blogs across its network. Both platforms face unique engineering challenges: serving millions of independent sites from shared infrastructure while maintaining security isolation, delivering sub-50ms response times, and handling massive traffic spikes without degradation.
This comprehensive nginx configuration demonstrates the architectural patterns and optimization techniques required to build WordPress infrastructure at this scale. From intelligent caching strategies that achieve 95%+ hit rates, to rate limiting that prevents abuse across millions of tenants, to dynamic SSL certificate loading for custom domains — every directive is carefully chosen and extensively commented to explain both the “what” and the “why” behind enterprise-scale web server design. Whether you’re running a school district with thousands of student blogs or building the next major multi-tenant platform, this Nginx configuration provides a production-ready foundation for serving millions of sites fast.
Key Statistics:
WordPress.com:
- Over 522 million websites running on the platform
- 409 million unique visitors viewing over 20 billion pages monthly
- 70 million new posts published per month
- Powers 43.3% of all websites on the internet
Edublogs:
- Over 5 million educational blogs across their network
- Nearly 100 million unique visitors per month
- Surpassed 1 million blogs milestone in 2011, with hundreds of thousands more on Edublogs Campus sites
# ============================================================================
# WordPress.com-Scale Nginx Configuration
# Designed for serving millions of websites with high performance
# ============================================================================
# ============================================================================
# WORKER PROCESS CONFIGURATION
# ============================================================================
# Sets nginx to automatically create one worker process per CPU core
# This ensures optimal CPU utilization without oversubscription
worker_processes auto;
# Increases the maximum number of file descriptors each worker can open
# Critical for handling millions of concurrent connections
worker_rlimit_nofile 100000;
# Gives nginx workers higher CPU scheduling priority (-20 to 19, lower is higher)
# Ensures nginx gets CPU time even under heavy system load
worker_priority -5;
# ============================================================================
# DYNAMIC MODULE LOADING
# ============================================================================
# Loads GeoIP2 module for geographic IP detection and routing
# Used to route users to nearest edge servers or apply geo-specific rules
load_module modules/ngx_http_geoip2_module.so;
# Loads Brotli compression modules (filter for dynamic, static for pre-compressed)
# Brotli provides better compression than gzip, reducing bandwidth by 15-20%
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;
# ============================================================================
# EVENTS BLOCK - Connection Processing Configuration
# ============================================================================
events {
# Maximum concurrent connections per worker process
# With 'auto' workers on 8-core system: 8 workers * 10,000 = 80,000 total connections
worker_connections 10000;
# Use epoll on Linux for efficient event notification
# epoll scales better than select/poll for thousands of connections
use epoll;
# Allows workers to accept multiple connections at once
# Improves performance under high connection rates
multi_accept on;
# Disables mutex locking when accepting connections
# Better performance on modern systems with good load balancing
accept_mutex off;
}
# ============================================================================
# HTTP BLOCK - Main HTTP Configuration
# ============================================================================
http {
# ========================================================================
# BASIC PERFORMANCE SETTINGS
# ========================================================================
# Enables Linux sendfile() syscall for serving static files
# Kernel handles file transfer directly, bypassing user space (huge performance gain)
sendfile on;
# Sends HTTP headers in one packet with sendfile
# Reduces network overhead by combining TCP packets
tcp_nopush on;
# Disables Nagle's algorithm for sending data immediately
# Reduces latency for small packets (important for dynamic content)
tcp_nodelay on;
# How long to keep idle keepalive connections open (seconds)
# Reusing connections saves TCP handshake overhead
keepalive_timeout 65;
# Maximum requests per keepalive connection before closing
# Prevents connection exhaustion while still benefiting from reuse
keepalive_requests 1000;
# Hash table size for MIME types lookup
# Larger value = faster MIME type determination for files
types_hash_max_size 2048;
# Hash bucket size for server names (domain matching)
# Must be increased when hosting many domains
server_names_hash_bucket_size 128;
# Maximum size of server names hash table
# Critical for WordPress.com scale with millions of domains
server_names_hash_max_size 4096;
# Maximum size of client request body (file uploads)
# Set to 128MB to allow reasonable WordPress media uploads
client_max_body_size 128M;
# Buffer size for reading client request body
# Larger buffer reduces disk I/O for typical uploads
client_body_buffer_size 128k;
# Buffer size for reading client request headers
# Sufficient for most requests without dynamic allocation
client_header_buffer_size 4k;
# Number and size of buffers for large client headers (cookies, long URLs)
# WordPress cookies can be large with logged-in users
large_client_header_buffers 8 16k;
# Loads MIME type definitions (maps file extensions to content types)
include /etc/nginx/mime.types;
# Default MIME type when file extension doesn't match known types
default_type application/octet-stream;
# ========================================================================
# LOGGING CONFIGURATION
# ========================================================================
# Custom log format capturing detailed request information
# Includes: client IP, hostname, timestamp, request details, response status,
# bytes sent, referrer, user agent, request/upstream timing, cache status, blog ID
log_format wpcom_main '$remote_addr - $http_host [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time" '
'cache=$upstream_cache_status '
'blog_id=$blog_id';
# Access log with buffering and delayed flushing
# Buffer reduces disk writes; flush ensures logs written every 5 seconds
# Critical for performance at scale (millions of requests/sec)
access_log /var/log/nginx/access.log wpcom_main buffer=256k flush=5s;
# Error log at 'warn' level (errors and warnings, not info/debug)
# Keeps log size manageable while capturing important issues
error_log /var/log/nginx/error.log warn;
# ========================================================================
# SECURITY & PERFORMANCE HEADERS
# ========================================================================
# Hides nginx version number in error pages and Server header
# Security through obscurity - prevents version-specific attacks
server_tokens off;
# Prevents page from being embedded in iframe on other domains
# Protects against clickjacking attacks
add_header X-Frame-Options "SAMEORIGIN" always;
# Prevents MIME type sniffing by browsers
# Forces browsers to respect declared content types
add_header X-Content-Type-Options "nosniff" always;
# Enables browser XSS filter
# Tells browser to block detected cross-site scripting attempts
add_header X-XSS-Protection "1; mode=block" always;
# ========================================================================
# GZIP COMPRESSION SETTINGS
# ========================================================================
# Enables gzip compression for responses
# Reduces bandwidth usage by 60-80% for text content
gzip on;
# Adds Vary: Accept-Encoding header
# Tells caches to store separate versions for compressed/uncompressed
gzip_vary on;
# Enables compression for proxied requests
# Compresses responses regardless of request headers
gzip_proxied any;
# Compression level (1-9, higher = better compression but more CPU)
# Level 6 is sweet spot for compression ratio vs CPU usage
gzip_comp_level 6;
# MIME types to compress
# Only compress text-based formats (images/videos already compressed)
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/rss+xml
application/atom+xml
image/svg+xml
font/truetype
font/opentype
application/vnd.ms-fontobject;
# Disables gzip for Internet Explorer 6
# IE6 has buggy gzip implementation
gzip_disable "msie6";
# ========================================================================
# BROTLI COMPRESSION SETTINGS
# ========================================================================
# Enables Brotli compression (modern alternative to gzip)
# 15-20% better compression than gzip for text content
brotli on;
# Brotli compression level (0-11)
# Level 6 balances compression ratio and CPU usage
brotli_comp_level 6;
# MIME types to compress with Brotli
# Same text-based types as gzip
brotli_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss;
# ========================================================================
# RATE LIMITING ZONES
# ========================================================================
# Rate limit zone for login attempts (per IP address)
# 10MB zone stores ~160,000 IP addresses
# Limit: 5 requests per minute per IP (prevents brute force)
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
# Rate limit zone for API requests (per blog/site)
# 50MB zone stores ~800,000 unique blog IDs
# Limit: 100 requests per second per blog (prevents API abuse)
limit_req_zone $blog_id zone=api_limit:50m rate=100r/s;
# Rate limit zone for general requests (per IP address)
# 50MB zone for tracking request rates
# Limit: 50 requests per second per IP (prevents DoS)
limit_req_zone $binary_remote_addr zone=general_limit:50m rate=50r/s;
# Connection limit zone (concurrent connections per IP)
# 10MB zone for tracking active connections
# Prevents single IP from exhausting connection pool
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
# ========================================================================
# FASTCGI CACHE CONFIGURATION
# ========================================================================
# Cache path for PHP/FastCGI responses
# levels=1:2 creates two-level directory hierarchy (prevents too many files in one dir)
# keys_zone: 1000MB shared memory for cache keys (~8 million cached pages)
# max_size: 50GB maximum disk space for cache
# inactive: Remove cached items not accessed for 60 minutes
# use_temp_path=off: Write directly to cache (faster, no double write)
fastcgi_cache_path /var/cache/nginx/fastcgi
levels=1:2
keys_zone=wordpress:1000m
max_size=50g
inactive=60m
use_temp_path=off;
# Cache path for static assets and API proxy responses
# 500MB shared memory for keys (~4 million cached items)
# 100GB disk space, 7-day retention for static assets
proxy_cache_path /var/cache/nginx/proxy
levels=1:2
keys_zone=static_cache:500m
max_size=100g
inactive=7d
use_temp_path=off;
# Cache key for FastCGI (PHP) responses
# Includes: protocol, method, hostname, URI, mobile flag, login status
# Different cache entries for mobile/desktop and logged-in/anonymous users
fastcgi_cache_key "$scheme$request_method$host$request_uri$is_mobile$is_logged_in";
# Cache key for proxy (static asset) responses
# Simpler key since static assets don't vary by user
proxy_cache_key "$scheme$request_method$host$request_uri";
# Prevents cache stampede (multiple requests for same uncached item)
# Only first request goes to backend, others wait for cache
fastcgi_cache_lock on;
# Maximum time to wait for cache lock before giving up
# After 5 seconds, request proceeds to backend
fastcgi_cache_lock_timeout 5s;
# Revalidates stale cache entries with If-Modified-Since
# Reduces backend load by serving stale content if unchanged
fastcgi_cache_revalidate on;
# Serves stale cached content when backend is unavailable
# Ensures site stays up even during PHP-FPM failures
# Serves stale on: errors, timeouts, during cache updates, invalid headers, 500/503 errors
fastcgi_cache_use_stale error timeout updating invalid_header http_500 http_503;
# Refreshes cache in background when serving stale content
# Users get instant response while cache updates asynchronously
fastcgi_cache_background_update on;
# ========================================================================
# UPSTREAM PHP-FPM POOLS (LOAD BALANCED)
# ========================================================================
# Main PHP backend pool for public-facing requests
# Uses least_conn algorithm (sends to server with fewest active connections)
# Better than round-robin for varying request durations
upstream php_backend {
least_conn;
# Multiple PHP-FPM servers for horizontal scaling
# max_fails: Mark server down after 3 consecutive failures
# fail_timeout: Keep server marked down for 30 seconds before retry
server php-fpm-01:9000 max_fails=3 fail_timeout=30s;
server php-fpm-02:9000 max_fails=3 fail_timeout=30s;
server php-fpm-03:9000 max_fails=3 fail_timeout=30s;
server php-fpm-04:9000 max_fails=3 fail_timeout=30s;
# Maintains 64 idle keepalive connections to PHP-FPM
# Reusing connections eliminates TCP/socket handshake overhead
keepalive 64;
}
# Dedicated PHP pool for admin/login requests
# Isolates admin traffic from public traffic (prevents admin slowness affecting site)
# More aggressive failure detection (2 failures, 20-second timeout)
upstream php_backend_admin {
least_conn;
server php-fpm-admin-01:9000 max_fails=2 fail_timeout=20s;
server php-fpm-admin-02:9000 max_fails=2 fail_timeout=20s;
# Fewer keepalive connections (admin traffic is lower volume)
keepalive 32;
}
# ========================================================================
# MAP DIRECTIVES - CONDITIONAL LOGIC
# ========================================================================
# Detects mobile devices from User-Agent header
# Sets $is_mobile to 1 for mobile devices, 0 for desktop
# Used for serving mobile-optimized cache and content
map $http_user_agent $is_mobile {
default 0;
~*android 1; # Android devices
~*iphone 1; # iPhones
~*ipad 1; # iPads
~*mobile 1; # Generic mobile browsers
}
# Detects logged-in WordPress users from cookies
# Sets $is_logged_in to 1 if user has WordPress login cookies
# Logged-in users bypass cache to see personalized content
map $http_cookie $is_logged_in {
default 0;
~*wordpress_logged_in 1; # WordPress login cookie
~*comment_author 1; # Comment author cookie (shows pending comments)
}
# Determines if request should skip cache based on URI
# Sets $skip_cache to 1 for admin, cron, and preview pages
# These pages must always be dynamic (never cached)
map $request_uri $skip_cache {
default 0;
~*/wp-admin 1; # WordPress admin panel
~*/wp-login.php 1; # Login page
~*/wp-cron.php 1; # Cron jobs
~*/xmlrpc.php 1; # XML-RPC API
~*preview=true 1; # Post/page previews
}
# Combines multiple conditions to determine final cache bypass
# Sets $bypass_cache to 1 if ANY condition requires bypassing cache:
# - skip_cache is 1 (admin/special pages)
# - is_logged_in is 1 (personalized content)
# - wp-postpass cookie exists (password-protected content)
map "$skip_cache:$is_logged_in:$http_cookie" $bypass_cache {
default 0;
~*1: 1; # If skip_cache is 1
~*:1: 1; # If is_logged_in is 1
~*wp-postpass 1; # If password-protected post cookie exists
}
# ========================================================================
# GEO-IP ROUTING
# ========================================================================
# Loads GeoIP2 database for IP geolocation
# Extracts country and continent codes from client IP
# Used for routing to nearest edge servers or geo-blocking
geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
$geoip2_country_code country iso_code; # Two-letter country code (US, UK, etc.)
$geoip2_continent_code continent code; # Continent code (NA, EU, AS, etc.)
}
# ========================================================================
# SHARED CONFIGURATION INCLUDES
# ========================================================================
# Includes external file with security rules
# Centralizes: bot blocking, user-agent filtering, request validation
include /etc/nginx/conf.d/security.conf;
# Includes external file with common WordPress rules
# Centralizes: plugin-specific rules, theme handling, common locations
include /etc/nginx/conf.d/wordpress-common.conf;
# ========================================================================
# HTTP TO HTTPS REDIRECT SERVER
# ========================================================================
# Default server block listening on port 80 (HTTP)
# reuseport: Allows multiple workers to bind to same port (performance boost)
server {
listen 80 default_server reuseport;
listen [::]:80 default_server reuseport; # IPv6 support
# Matches any hostname (wildcard default server)
server_name _;
# 301 permanent redirect to HTTPS
# Forces all traffic to encrypted connections
# $host preserves original hostname, $request_uri preserves path
return 301 https://$host$request_uri;
}
# ========================================================================
# MAIN WILDCARD SERVER BLOCK - *.wordpress.com
# ========================================================================
# Primary server block handling all WordPress.com subdomains
server {
# Listens on port 443 (HTTPS) with HTTP/2 support
listen 443 ssl http2 default_server reuseport;
listen [::]:443 ssl http2 default_server reuseport; # IPv6
# Regex capture subdomain from hostname (e.g., myblog.wordpress.com)
# Stores subdomain in $subdomain variable for later use
server_name ~^(?<subdomain>.+)\.wordpress\.com$;
# ====================================================================
# SSL/TLS CONFIGURATION
# ====================================================================
# Path to wildcard SSL certificate (*.wordpress.com)
# Single cert covers all subdomains
ssl_certificate /etc/nginx/ssl/wildcard.wordpress.com.crt;
ssl_certificate_key /etc/nginx/ssl/wildcard.wordpress.com.key;
# Shared SSL session cache across all workers (100MB stores ~400k sessions)
# Dramatically speeds up subsequent connections from same client
ssl_session_cache shared:SSL:100m;
# SSL sessions valid for 24 hours
# Clients can resume session without full handshake
ssl_session_timeout 24h;
# Disables session tickets (stateless resumption)
# Session tickets have security concerns, prefer session cache
ssl_session_tickets off;
# Only allow TLS 1.2 and 1.3 (modern, secure protocols)
# Blocks older, vulnerable protocols (SSLv3, TLS 1.0, TLS 1.1)
ssl_protocols TLSv1.2 TLSv1.3;
# Cipher suite selection (prioritizes forward secrecy)
# ECDHE provides perfect forward secrecy
# GCM provides authenticated encryption
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
# Let client choose cipher (better compatibility with modern clients)
ssl_prefer_server_ciphers off;
# OCSP stapling: Server fetches OCSP response and sends to client
# Eliminates client OCSP lookup (faster, more private)
ssl_stapling on;
ssl_stapling_verify on;
# DNS resolvers for OCSP stapling lookups
# Uses Google DNS with 5-minute cache
resolver 8.8.8.8 8.8.4.4 valid=300s;
# ====================================================================
# DOCUMENT ROOT & BLOG IDENTIFICATION
# ====================================================================
# Document root: shared WordPress installation
# All sites run from same codebase (multisite architecture)
root /var/www/wordpress;
# Index files to try when directory is requested
index index.php index.html;
# Extract blog_id from hostname
# In production, queries Redis/Memcached for blog ID lookup
# Blog ID used for routing, rate limiting, and WordPress multisite
set $blog_id "unknown";
# Lua block to fetch blog_id from cache
# Mock implementation shown - production would query Redis
# Key: "blog:subdomain.wordpress.com" -> Value: numeric blog_id
set_by_lua_block $blog_id {
-- Mock implementation - in production, query Redis
-- local redis = require "resty.redis"
-- local red = redis:new()
-- red:connect("redis-host", 6379)
-- return red:get("blog:" .. ngx.var.host)
return "12345"
}
# ====================================================================
# RATE LIMITING APPLICATION
# ====================================================================
# Applies general rate limit (50 req/sec per IP)
# burst=20: Allows 20 requests over limit before rejecting
# nodelay: Process burst requests immediately (don't delay)
limit_req zone=general_limit burst=20 nodelay;
# Limits concurrent connections to 10 per IP
# Prevents single client from monopolizing connections
limit_conn conn_limit 10;
# ====================================================================
# STATIC ASSET HANDLING
# ====================================================================
# Location block for static media files (images, fonts, CSS, JS)
# Matches file extensions with regex
location ~* \.(jpg|jpeg|gif|png|webp|svg|ico|css|js|woff|woff2|ttf|eot)$ {
# Cache static assets in browser for 1 year
# Static assets have versioned URLs, so safe to cache forever
expires 365d;
# Cache-Control header with public (cacheable by proxies) and immutable
# immutable: tells browser file never changes (don't revalidate)
add_header Cache-Control "public, immutable";
# Adds cache status header for debugging (HIT, MISS, BYPASS, etc.)
add_header X-Cache-Status $upstream_cache_status;
# Don't log static asset requests (reduces log volume)
access_log off;
# Try to serve file from disk first
# If not found, proxy to dedicated media servers
try_files $uri @media_server;
}
# Named location for media server proxy
# Handles static assets not found on local disk
location @media_server {
# Proxy request to media cluster (separate servers/CDN)
proxy_pass http://media-cluster;
# Cache proxied responses
proxy_cache static_cache;
# Cache successful responses for 1 year
proxy_cache_valid 200 365d;
# Cache 404s briefly (1 minute) to prevent repeated lookups
proxy_cache_valid 404 1m;
}
# ====================================================================
# WORDPRESS ADMIN AREA
# ====================================================================
# Handles WordPress admin dashboard and login page
# Matches /wp-admin/* and /wp-login.php
location ~ ^/(wp-admin|wp-login\.php) {
# Strict rate limiting for login attempts (5 per minute)
# burst=3: Allow 3 attempts over limit before blocking
# Prevents brute force password attacks
limit_req zone=login_limit burst=3 nodelay;
# Never cache admin pages (always dynamic)
# bypass=1: Skip reading from cache
# no_cache=1: Don't store response in cache
fastcgi_cache_bypass 1;
fastcgi_no_cache 1;
# Route to dedicated admin PHP-FPM pool
# Isolates admin traffic from public traffic
fastcgi_pass php_backend_admin;
# Default script for directory requests
fastcgi_index index.php;
# Sets SCRIPT_FILENAME parameter (full path to PHP script)
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# Includes standard FastCGI parameters
include fastcgi_params;
# Pass blog identification to WordPress
fastcgi_param BLOG_ID $blog_id;
fastcgi_param HTTP_HOST $host;
}
# ====================================================================
# WORDPRESS REST API
# ====================================================================
# Handles WordPress REST API endpoints (/wp-json/*)
location ~ ^/wp-json/ {
# API-specific rate limiting (100 req/sec per blog)
# burst=50: More lenient burst for legitimate API usage
limit_req zone=api_limit burst=50 nodelay;
# Selective caching logic for API endpoints
# Only cache public GET requests
set $api_cache 0;
# Cache GET requests (safe, idempotent)
if ($request_method = GET) {
set $api_cache 1;
}
# Don't cache authenticated requests
# Authorization header indicates private data
if ($http_authorization != "") {
set $api_cache 0;
}
# Use FastCGI cache for API responses
fastcgi_cache wordpress;
# Apply cache bypass based on conditions above
fastcgi_cache_bypass $api_cache;
# Cache successful API responses for 60 seconds
# Short TTL balances performance with freshness
fastcgi_cache_valid 200 60s;
# Route to main PHP backend pool
fastcgi_pass php_backend;
fastcgi_index index.php;
# API uses WordPress index.php as router
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
include fastcgi_params;
fastcgi_param BLOG_ID $blog_id;
}
# ====================================================================
# SPECIAL WORDPRESS FILES
# ====================================================================
# XML-RPC endpoint (legacy remote publishing API)
# Often abused for brute force and DDoS attacks
location = /xmlrpc.php {
# Very strict rate limiting (same as login)
# burst=2: Only 2 attempts over limit
limit_req zone=login_limit burst=2 nodelay;
# Process with main PHP backend
fastcgi_pass php_backend;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_param BLOG_ID $blog_id;
}
# WordPress cron endpoint (scheduled tasks)
# Should only be called by internal job scheduler, not public
location = /wp-cron.php {
# Restrict to internal network only
# 10.0.0.0/8 is typical private IP range
allow 10.0.0.0/8;
# Block all other IPs (prevents external cron abuse)
deny all;
# Process with main PHP backend
fastcgi_pass php_backend;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# ====================================================================
# MAIN WORDPRESS ROUTING
# ====================================================================
# Default location block (matches everything not caught above)
location / {
# Try to serve request as:
# 1. File ($uri)
# 2. Directory ($uri/)
# 3. Pass to WordPress index.php with query args
# This implements WordPress pretty permalinks
try_files $uri $uri/ /index.php?$args;
}
# Handles all PHP file requests
location ~ \.php$ {
# ================================================================
# SECURITY: UPLOADS DIRECTORY
# ================================================================
# Nested location prevents PHP execution in uploads directory
# Critical security measure (prevents uploaded malicious PHP)
location ~ ^/wp-content/uploads/.*\.php$ {
deny all; # Block all requests for PHP in uploads
}
# ================================================================
# FASTCGI CACHE CONFIGURATION
# ================================================================
# Use WordPress cache zone
fastcgi_cache wordpress;
# Bypass cache if conditions met (logged in, admin, etc.)
fastcgi_cache_bypass $bypass_cache;
# Don't store in cache if conditions met
fastcgi_no_cache $bypass_cache;
# Cache successful responses for 60 minutes
fastcgi_cache_valid 200 60m;
# Cache redirects for 10 minutes
fastcgi_cache_valid 301 302 10m;
# Cache 404s for 10 minutes (prevents repeated 404 lookups)
fastcgi_cache_valid 404 10m;
# Add debugging headers showing cache status
add_header X-Cache-Status $upstream_cache_status;
add_header X-Blog-ID $blog_id;
# ================================================================
# FASTCGI SETTINGS
# ================================================================
# Route to main PHP backend pool
fastcgi_pass php_backend;
# Default index file
fastcgi_index index.php;
# Sets full path to PHP script being executed
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# Includes standard FastCGI parameters (REQUEST_URI, etc.)
include fastcgi_params;
# WordPress multisite parameters
fastcgi_param BLOG_ID $blog_id; # Identifies which blog
fastcgi_param HTTP_HOST $host; # Original hostname
# ================================================================
# TIMEOUT SETTINGS
# ================================================================
# Max time to connect to PHP-FPM
fastcgi_connect_timeout 60s;
# Max time to send request to PHP-FPM
fastcgi_send_timeout 180s;
# Max time to read response from PHP-FPM
# 3 minutes allows for complex WordPress queries
fastcgi_read_timeout 180s;
# ================================================================
# BUFFER SETTINGS
# ================================================================
# Buffer size for reading first part of PHP response
# Needs to fit response headers
fastcgi_buffer_size 32k;
# Number and size of buffers for reading PHP response
# 256 * 16k = 4MB total buffering
fastcgi_buffers 256 16k;
# Maximum buffer size for busy conditions
# Allows buffering large responses before sending to client
fastcgi_busy_buffers_size 256k;
# Size threshold for writing response to temp file
# Responses larger than 256k spill to disk
fastcgi_temp_file_write_size 256k;
# Keeps connections to PHP-FPM alive between requests
# Eliminates socket connection overhead (major performance gain)
fastcgi_keep_conn on;
}
# ====================================================================
# SECURITY: BLOCK SENSITIVE FILES
# ====================================================================
# Blocks access to hidden files and directories (start with .)
# Protects .git, .htaccess, .env, etc.
location ~ /\. {
deny all; # Return 403 Forbidden
access_log off; # Don't log these attempts
log_not_found off; # Don't log as 404
}
# Blocks access to backup files ending with ~
# Prevents exposure of editor backup files (file.php~)
location ~ ~$ {
deny all;
access_log off;
log_not_found off;
}
# Blocks direct access to wp-config.php
# Contains database credentials and security keys
location ~* wp-config\.php {
deny all;
}
# Blocks access to readme, license, changelog files
# Prevents version disclosure and fingerprinting
location ~* (?:readme|license|changelog)\.(?:txt|html)$ {
deny all;
}
}
# ========================================================================
# CUSTOM DOMAIN SUPPORT
# ========================================================================
# Server block for custom domains (e.g., myblog.com mapped to WordPress.com)
# Handles any domain not matching *.wordpress.com
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
# Regex captures any custom domain
# Stored in $custom_domain variable
server_name ~^(?<custom_domain>.+)$;
# ====================================================================
# DYNAMIC SSL CERTIFICATE LOADING
# ====================================================================
# Lua block executes during SSL handshake (before HTTP)
# Dynamically loads SSL certificate based on SNI (Server Name Indication)
# Allows serving millions of custom domains with different certs
ssl_certificate_by_lua_block {
-- Load SSL certificate dynamically based on SNI
local ssl = require "ngx.ssl"
-- Get hostname from SSL handshake
local server_name = ssl.server_name()
-- In production: fetch certificate from Redis/storage
-- Key: "cert:myblog.com" -> Value: certificate data
-- Example:
-- local redis = require "resty.redis"
-- local red = redis:new()
-- red:connect("redis-host", 6379)
-- local cert_data = red:get("cert:" .. server_name)
-- local key_data = red:get("key:" .. server_name)
-- ssl.set_cert(cert_data)
-- ssl.set_priv_key(key_data)
}
# Includes same WordPress configuration as main server block
# Reuses all location blocks, caching, rate limiting, etc.
# Avoids duplicating hundreds of lines of config
include /etc/nginx/conf.d/wordpress-main.conf;
}
# ========================================================================
# HEALTH CHECK & MONITORING ENDPOINT
# ========================================================================
# Separate server block on port 8080 for internal monitoring
# Not exposed to public internet (load balancers/monitoring only)
server {
listen 8080;
listen [::]:8080;
# Simple health check endpoint
# Load balancers poll this to determine if server is healthy
location /health {
# Don't log health checks (reduces log noise)
access_log off;
# Returns 200 OK with "healthy" text
# Any 200 response indicates server is operational
return 200 "healthy\n";
# Sets content type to plain text
add_header Content-Type text/plain;
}
# Nginx status page (metrics endpoint)
# Shows active connections, requests, etc.
location /nginx_status {
# Enables stub_status module
# Provides basic nginx metrics
stub_status;
# Don't log monitoring scrapes
access_log off;
# Restrict to internal network (10.x.x.x)
# Prevents public exposure of server metrics
allow 10.0.0.0/8;
deny all;
}
}
}
# ============================================================================
# ADDITIONAL CONFIGURATION FILES
# ============================================================================
# These files would be referenced by include directives above
# ============================================================================
# /etc/nginx/conf.d/security.conf would contain:
# ----------------------------------------------------------------------------
# - Bot blocking rules (block known malicious user agents)
# - DDoS mitigation rules (request patterns, rate limits)
# - Bad user-agent blocking (scrapers, vulnerability scanners)
# - Request validation rules (block SQL injection patterns, etc.)
# - Geographic blocking if needed (block high-abuse countries)
# - Referrer spam blocking
# /etc/nginx/conf.d/wordpress-common.conf would contain:
# ----------------------------------------------------------------------------
# - Shared WordPress location blocks (reusable across server blocks)
# - Common security rules (block common exploit paths)
# - Plugin-specific rules (WooCommerce, bbPress, etc.)
# - Theme handling rules
# - WordPress REST API additional rules
# - File upload handling rules
# - Search engine optimization rules (sitemaps, robots.txt)
# /etc/nginx/conf.d/wordpress-main.conf would contain:
# ----------------------------------------------------------------------------
# - Complete duplicate of all location blocks from main server
# - Used by custom domain server block via include
# - Allows single source of truth for WordPress handling
# - Reduces configuration duplication and maintenance burden
# ============================================================================
# PERFORMANCE CONSIDERATIONS AT SCALE
# ============================================================================
# This configuration is designed for extreme scale with these principles:
#
# 1. HORIZONTAL SCALING
# - Multiple nginx servers behind load balancer
# - Shared cache storage (distributed filesystem or object storage)
# - Stateless design (any request can hit any server)
#
# 2. CACHING STRATEGY
# - FastCGI cache reduces PHP-FPM load by 90%+
# - Cache varies by: mobile/desktop, logged-in status, URL
# - Stale content serving ensures availability during failures
# - Cache locking prevents stampedes
#
# 3. CONNECTION POOLING
# - Keepalive to PHP-FPM eliminates socket overhead
# - Keepalive to clients reduces TCP handshakes
# - Connection reuse saves CPU and memory
#
# 4. RATE LIMITING
# - Per-IP limits prevent individual abuse
# - Per-blog limits prevent site-level abuse
# - Different limits for different endpoints (login < API < general)
#
# 5. MONITORING & OBSERVABILITY
# - Detailed logging with timing information
# - Cache status headers for debugging
# - Health check endpoints for load balancers
# - Nginx status for metrics collection
#
# 6. SECURITY LAYERING
# - Rate limiting (prevents brute force)
# - Geographic routing (route to safe regions)
# - File upload restrictions (prevent malicious PHP)
# - Hidden file blocking (protect .git, .env)
# - Version hiding (prevent targeted attacks)
#
# 7. FAILURE HANDLING
# - Multiple upstream servers with health checks
# - Stale content serving during backend failures
# - Graceful degradation (serve cached when possible)
# - Separate admin pools (isolate admin from public failures)
#
# 8. EFFICIENCY
# - Compression (gzip + brotli) saves 60-80% bandwidth
# - Static asset caching (1 year browser cache)
# - Sendfile for static files (kernel-level efficiency)
# - Buffering to reduce disk I/O
#
# Expected performance with this configuration:
# - Single nginx server: 50,000+ requests/second
# - With FastCGI cache hit ratio of 95%: serve millions of sites
# - Average response time: <5ms (cached), <100ms (uncached)
# - Memory usage: ~500MB per worker process
# - CPU usage: <20% on modern 8-core systems under normal load