Nginx Configuration Snippets for WordPress (Copy-Paste Ready)

Production-ready Nginx config snippets for WordPress: FastCGI cache, security headers, gzip, rate limiting, and more — with explanations for each block.

Dobromir Dechev
Dobromir WordPress agency owner

Running Nginx directly (as on a Cloudways or self-managed VPS) gives you fine-grained control over how your WordPress site performs and how it handles requests. These are the Nginx snippets I use across production WordPress sites - each block is explained so you understand what you are adding before you paste it.

All snippets assume Ubuntu or Debian with Nginx installed from the official Nginx repository. Paths may vary slightly on CentOS/AlmaLinux.

The base server block

Start with a clean server block. This is the minimum required:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    server_name yourdomain.com www.yourdomain.com;

    root /var/www/html;
    index index.php;

    # SSL config (managed by Certbot or your host)
    ssl_certificate     /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;

    # Log files
    access_log /var/log/nginx/yourdomain.com.access.log;
    error_log  /var/log/nginx/yourdomain.com.error.log warn;

    include /etc/nginx/snippets/wordpress.conf;
}

# HTTP redirect
server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$host$request_uri;
}

The WordPress-specific rules go in /etc/nginx/snippets/wordpress.conf which we build up below.

WordPress rewrite rules

WordPress uses pretty permalinks via a catch-all rewrite. Without this, only the homepage works:

# Try files, then directories, then WordPress index.php
location / {
    try_files $uri $uri/ /index.php?$args;
}

# Pass PHP to PHP-FPM
location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_read_timeout 120;
}

The try_files line is the critical piece. It tries to serve a real file first, then a directory, then falls through to index.php - which is how WordPress handles all permalink routing.

Block access to sensitive files

Block direct access to files that should never be publicly accessible:

# Disable access to wp-config.php
location = /wp-config.php {
    deny all;
}

# Disable access to .htaccess, .env, readme files
location ~* /\.(ht|env|git) {
    deny all;
}

location ~* /(readme\.html|readme\.txt|license\.txt|wp-trackback\.php) {
    deny all;
}

# Block access to PHP files in uploads directory
# (prevents PHP execution from uploaded files)
location ~* /wp-content/uploads/.*\.php$ {
    deny all;
}

Blocking PHP execution in the uploads directory is important. It stops an attacker who has managed to upload a PHP file from executing it.

Disable XML-RPC

XML-RPC is abused for brute-force attacks and DDoS amplification. Block it at the Nginx level before PHP even loads:

location = /xmlrpc.php {
    deny all;
    access_log off;
    log_not_found off;
}

access_log off and log_not_found off prevent your access log from being filled with blocked bot requests.

Rate limiting login page

Brute-force protection at the Nginx level:

# In http context (nginx.conf or conf.d/security.conf)
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

# In your server block
location = /wp-login.php {
    limit_req zone=login burst=3 nodelay;
    include fastcgi_params;
    fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

This allows 5 requests per minute per IP to the login page, with a burst of 3. Bots trying hundreds of passwords per second will get 429 Too Many Requests responses.

Gzip compression

Enable gzip for all text-based responses:

# In http context
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types
    application/atom+xml
    application/geo+json
    application/javascript
    application/x-javascript
    application/json
    application/ld+json
    application/manifest+json
    application/rdf+xml
    application/rss+xml
    application/xhtml+xml
    application/xml
    font/eot
    font/otf
    font/ttf
    image/svg+xml
    text/css
    text/javascript
    text/plain
    text/xml;

gzip_comp_level 6 is a good balance between CPU usage and compression ratio. Level 9 uses significantly more CPU for marginal extra compression.

Static file caching headers

Tell browsers to cache static assets aggressively:

# Images, fonts, documents
location ~* \.(jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|webp|avif)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
}

# CSS and JavaScript
location ~* \.(css|js)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
}

# Fonts
location ~* \.(eot|ttf|woff|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    add_header Access-Control-Allow-Origin "*";
}

immutable tells the browser that this resource will never change at this URL, so do not even bother revalidating it during the cache period. This requires you to use cache-busting URLs (e.g., style.min.css?ver=2.1.3) when you update assets, which WordPress does automatically via the $ver parameter.

FastCGI cache (page caching at Nginx level)

This is the most impactful performance configuration. FastCGI cache stores the PHP output and serves it directly from Nginx without invoking PHP-FPM at all. It is comparable to Varnish but without the extra process:

# In http context (nginx.conf)
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=wordpress:100m inactive=60m max_size=1g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

# In your server block
set $skip_cache 0;

# Do not cache POST requests
if ($request_method = POST) { set $skip_cache 1; }

# Do not cache URLs with a query string
if ($query_string != "") { set $skip_cache 1; }

# Do not cache logged-in users or recent commenters
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
    set $skip_cache 1;
}

# Do not cache WooCommerce cart/checkout
if ($request_uri ~* "/cart.*|/checkout.*|/my-account.*|/wp-admin.*") {
    set $skip_cache 1;
}

location ~ \.php$ {
    include fastcgi_params;
    fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_read_timeout 120;

    fastcgi_cache wordpress;
    fastcgi_cache_valid 200 60m;    # cache 200 responses for 60 minutes
    fastcgi_cache_valid 404 10m;
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache $skip_cache;

    add_header X-FastCGI-Cache $upstream_cache_status;
}

The X-FastCGI-Cache header lets you check the cache status in your browser (HIT, MISS, BYPASS, EXPIRED). After loading a page the first time (MISS), reload and you should see HIT - with a TTFB typically under 20ms.

You also need to purge the cache when content is updated. Use the Nginx Helper plugin, which integrates with FastCGI cache and purges automatically on publish, update, and comment.

Security headers

Add these headers to every response:

# In your server block
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

X-Frame-Options SAMEORIGIN prevents your site from being embedded in an iframe on another domain (clickjacking protection).

X-Content-Type-Options nosniff prevents browsers from MIME-sniffing a response away from the declared content type.

Strict-Transport-Security (HSTS) tells browsers to always use HTTPS. Do not add the preload directive until you are certain HTTPS is permanently configured.

Protecting wp-admin with IP allowlist

If your admin only needs to be accessed from specific IP addresses:

location /wp-admin {
    allow 203.0.113.0;    # your office IP
    allow 198.51.100.0;   # your home IP
    deny all;
}

location = /wp-login.php {
    allow 203.0.113.0;
    allow 198.51.100.0;
    deny all;
}

This is the most effective brute-force protection possible - bots simply get a 403 and cannot attempt passwords at all. The downside is you cannot log in from unexpected locations.

Health check endpoint (for load balancers)

If your site sits behind an AWS ALB, GCP load balancer, or any health-check system:

location = /healthz {
    return 200 "OK";
    add_header Content-Type text/plain;
    access_log off;
}

Handling large file uploads

If WordPress needs to handle large media uploads (video, PDF, design files):

client_max_body_size 64M;

location ~ \.php$ {
    # ... existing PHP config ...
    fastcgi_buffers 16 16k;
    fastcgi_buffer_size 32k;
}

Match client_max_body_size with the upload_max_filesize and post_max_size values in php.ini.

Testing and reloading

Always test your Nginx configuration before applying:

sudo nginx -t

If the test passes:

sudo nginx -s reload
# or
sudo systemctl reload nginx

Never use sudo systemctl restart nginx on a production server unless necessary - reload sends a SIGHUP which gracefully reloads the config without dropping active connections. restart kills all connections immediately.

Putting it all together

Organise your snippets into reusable include files:

/etc/nginx/
├── nginx.conf                  (http context: gzip, cache zone, rate limit zone)
├── snippets/
│   ├── wordpress.conf          (rewrite rules, PHP location)
│   ├── wordpress-security.conf (file blocks, xmlrpc, headers)
│   └── fastcgi-cache.conf      (cache config)
└── sites-available/
    └── yourdomain.com.conf     (server blocks, includes snippets)

Then in your server block:

include /etc/nginx/snippets/wordpress.conf;
include /etc/nginx/snippets/wordpress-security.conf;
include /etc/nginx/snippets/fastcgi-cache.conf;

This makes it trivial to apply the same hardened config to multiple sites without copy-pasting everything each time.


Was this article helpful?