Step 4: Directory protection and housekeeping
These are the same drop.conf rules that apply to any site — deny hidden files, backup files, and common config extensions:
# drop.conf
# Let's Encrypt renewal path
location ~ /.well-known/acme-challenge {
root /usr/share/nginx/html;
allow all;
}
# Deny hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Deny backup files
location ~ ~$ {
deny all;
access_log off;
log_not_found off;
}
# Deny sensitive extensions
location ~* \.(yml|yaml|env|bak|swp|dist|config)$ {
deny all;
access_log off;
log_not_found off;
}
# Handle favicon.ico cleanly
location = /favicon.ico {
access_log off;
log_not_found off;
try_files $uri =204;
}
# Handle robots.txt — WordPress generates this dynamically
location = /robots.txt {
try_files $uri /index.php;
}
Step 5: Static file caching
WordPress themes and plugins ship a lot of static assets. Caching them aggressively at the Nginx level keeps PHP-FPM free for requests that actually need it:
# static_caching.conf
# Images and fonts — immutable, cache as long as possible
location ~* \.(png|jpg|jpeg|gif|ico|webp|woff2|woff|ttf)$ {
try_files $uri =404;
expires max;
add_header Pragma public;
add_header Cache-Control "public, no-transform";
}
# CSS, JS, SVG, text — cache aggressively but allow revalidation
location ~* \.(css|js|html|htm|txt|xml|svg)$ {
gzip_static on;
try_files $uri =404;
expires max;
add_header Pragma public;
add_header Cache-Control "public, must-revalidate, proxy-revalidate";
}
try_files $uri =404; in both blocks ensures missing static files return a clean 404 instead of falling through to WordPress — saving a PHP-FPM worker and a database query for every broken asset reference.
Step 6: PHP handler and FastCGI tuning
WordPress benefits from PHP-FPM tuning at both the Nginx and pool levels:
# php.conf
location ~ \.php$ {
try_files $uri =404;
fastcgi_intercept_errors on;
fastcgi_ignore_client_abort on;
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
}
At the nginx.conf http { } level, these FastCGI directives optimize how Nginx talks to PHP-FPM for a WordPress workload:
# In http { } — applies to all PHP locations
fastcgi_intercept_errors on;
fastcgi_ignore_client_abort on;
fastcgi_max_temp_file_size 0;
fastcgi_buffers 512 4k;
fastcgi_read_timeout 180;
fastcgi_intercept_errors on — Lets Nginx serve its own error pages instead of passing PHP error responses through. Cleaner 404s and 50x pages without involving WordPress.
fastcgi_ignore_client_abort on — PHP-FPM finishes processing even if the visitor closes their browser. Critical for wp-cron and scheduled tasks that are triggered by page loads — without it, a user who navigates away mid-request cancels the backend work.
fastcgi_max_temp_file_size 0 — Disables buffering PHP responses to disk. Keeps everything in memory and avoids I/O bottlenecks on busy sites. Only safe if your PHP responses fit within your buffer configuration (which they will for normal WordPress pages).
fastcgi_buffers 512 4k — 512 buffers of 4KB each (2MB total). Tuned for WordPress page responses that can be larger than Nginx's default buffer allocation — the admin area, plugin pages, and certain themes push more markup than a typical PHP app.
fastcgi_read_timeout 180 — 180-second timeout for PHP responses. Accommodates slow WordPress admin operations like plugin updates, media processing, and backup generation that can take well over the default 60 seconds.
On the PHP-FPM side, make sure cgi.fix_pathinfo = 0 in your php.ini. When enabled, PHP tries to guess which file you meant if the requested path doesn't exist. When disabled, it only executes the exact file requested — defense in depth alongside try_files $uri =404;.