← Back to Home

WordPress Nginx Configuration Complete Troubleshooting Guide

WordPressNginxconfiguration pitfall

On day one of running WordPress on Nginx, I changed permalinks to /%postname%/. Every page except the homepage returned 404.

This is not unusual. WordPress documentation defaults to Apache .htaccess rules. Nginx doesn't use .htaccess, so 90% of newcomers hit this wall immediately after migrating.

This post covers exactly 5 real Nginx server block pitfalls I've encountered—each with the actual error and the fix.

Pitfall 1: Missing `/index.php` in try_files Breaks All Permalinks

**Symptom**: Homepage works fine. Every post, category, and tag page returns 404 Not Found (Nginx's default 404, not your WordPress theme's 404).

**Root cause**: The try_files directive controls how Nginx routes requests. WordPress uses a front controller pattern—all non-static requests should go to index.php. Without the /index.php fallback, Nginx can't find the file and returns 404 directly.

Broken config:

location / {
    try_files $uri $uri/ =404;  # ← Missing PHP fallback
}

Correct config:

location / {
    try_files $uri $uri/ /index.php?$args;  # ← Critical: fall back to index.php
}

Don't omit $args. Without it, WordPress query parameters (pagination ?page=2, search ?s=keyword) are silently dropped.

**Verify**: After the change, run sudo nginx -t && sudo systemctl reload nginx, then visit any post URL. It should render normally.

Pitfall 2: Incomplete FastCGI Cache Bypass Leaks Cached Pages to Logged-in Users

Symptom: You log into wp-admin but see a cached visitor page. You edit a post but the admin panel shows stale content. WooCommerce carts display other users' items.

**Root cause**: Nginx's fastcgi_cache ignores cookies by default and caches everything indiscriminately. WordPress identifies logged-in users via wordpress_logged_in_* cookies, but Nginx doesn't know about them.

**Fix**: Add a cache bypass map in your http {} or server {} block:

# Add in http {} block
map $http_cookie $skip_cache {
    default 0;
    ~wordpress_logged_in 1;
    ~wp-postpass          1;
    ~woocommerce_items_in_cart 1;
}

# Use in the PHP location within server {} block
location ~ \.php$ {
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_cache WORDPRESS;
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache $skip_cache;
    # ... other fastcgi params
}

Key points:

WooCommerce extra care: Cart and checkout pages need additional exclusion:

map $uri $skip_cache_uri {
    default 0;
    ~^/cart      1;
    ~^/checkout  1;
    ~^/my-account 1;
}

Then merge variables: set $do_skip $skip_cache$skip_cache_uri; and use $do_skip instead of $skip_cache.

Pitfall 3: Missing Security Headers Cause Security Scan Failures

Symptom: securityheaders.com scores you an F. Webmaster tools warn about missing security headers.

**Root cause**: Nginx adds zero security response headers by default. WordPress doesn't handle this either—it relies on .htaccess plugins under Apache. On Nginx, you must add them manually.

**Add all 6 core security headers at once** (in your server {} block):

# Prevent MIME-type sniffing
add_header X-Content-Type-Options "nosniff" always;

# Prevent clickjacking
add_header X-Frame-Options "SAMEORIGIN" always;

# HSTS — only enable after SSL is fully working!
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# Control referrer information leakage
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Restrict browser features (camera/mic/location etc.)
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;

# XSS protection (for legacy browsers)
add_header X-XSS-Protection "1; mode=block" always;

⚠️ Three real traps here:

1. **The always keyword is mandatory**—without it, Nginx only returns these headers on 200/301 responses. On 404/500, they're omitted, and security scanners still dock points.

2. HSTS only belongs in the HTTPS server block—if you put it in the port 80 (HTTP) block, browsers ignore it.

3. **Don't blindly copy CSP (Content-Security-Policy)**—WordPress core + plugins + themes load tons of inline scripts. A simple script-src 'self' will white-screen your admin panel. Start with Content-Security-Policy-Report-Only to observe, then tighten gradually.

Pitfall 4: Unblocked xmlrpc.php Leads to Brute Force CPU Spikes

**Symptom**: Server CPU suddenly hits 100%. tail -f /var/log/nginx/access.log shows floods of POST /xmlrpc.php 200 requests.

Root cause: WordPress enables xmlrpc.php by default. Attackers use it for XML-RPC brute force—one request can try thousands of passwords, making it100x more efficient than traditional wp-login.php attacks.

Nginx block (add directly in your server block):

# Completely block xmlrpc.php
location = /xmlrpc.php {
    deny all;
}

If you use Jetpack: Jetpack depends on xmlrpc.php. Allow only Jetpack IPs:

location = /xmlrpc.php {
    allow 192.0.64.0/18;   # Jetpack/Automattic IP range
    deny all;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

**Verify**: curl -I https://yourdomain.com/xmlrpc.php should return 403 Forbidden.

Pitfall 5: PHP Upload Limits Silently Truncated at the Nginx Layer

**Symptom**: Uploading a theme or plugin shows "The uploaded file exceeds the upload_max_filesize directive in php.ini", even though you already changed php.ini to 64M.

**Root cause**: Nginx has its own request body size limit, defaulting to just 1MB. No matter how large you set PHP's upload_max_filesize and post_max_size, Nginx rejects the request before it reaches PHP.

Fix:

# In http {} or server {} block
client_max_body_size 64M;

The complete upload trifecta (both Nginx and PHP need changes):

# Nginx layer
client_max_body_size 64M;
; PHP layer (/etc/php/8.3/fpm/php.ini)
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300
memory_limit = 256M

Restart PHP-FPM after changing PHP settings: sudo systemctl restart php8.3-fpm.

My Complete Server Block Reference

Based on spinupwp/wordpress-nginx (the best-maintained WordPress-Nginx template on GitHub) and my own battle scars, here's a production-ready single-site configuration skeleton:

server {
    listen 443 ssl http2;
    server_name example.com;
    root /var/www/example.com/public;
    index index.php;

    # SSL (Let's Encrypt)
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Security headers
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

    # Upload limits
    client_max_body_size 64M;

    # WordPress permalinks
    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # Block xmlrpc
    location = /xmlrpc.php {
        deny all;
    }

    # Deny sensitive files
    location ~ /\. { deny all; }
    location ~* /wp-config.php { deny all; }

    # PHP processing + FastCGI cache
    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_cache WORDPRESS;
        fastcgi_cache_valid 200 301 302 60m;
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Static file caching
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 365d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
}

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

Version Reference (Verified June 2026)

ComponentVersion
Nginx1.28.2 stable (includes CVE-2026-1642 fix)
PHP-FPM8.3
WordPress6.8
Ubuntu24.04 LTS

Versions change. Configuration logic doesn't. Run nginx -v and php -v to confirm your own environment.

---

This post only covers Nginx server block pitfalls. If you're looking for WordPress database optimization traps, check out: WordPress wp_options Autoload Optimization.

If your WordPress runs in Docker, there are additional port mapping and network configuration traps—that's a whole different battlefield.

⚠️ This post contains affiliate links. If you purchase through these links, I may earn a small commission at no extra cost to you.

📌 This article was AI-assisted generated and human-reviewed | TechPassive — An AI-driven content testing site focused on real tool reviews

🔗 Recommended Tools

These are carefully selected tools. Using our affiliate links supports us to keep producing quality content:

☁️ DigitalOcean Cloud ⚡ Vultr VPS 📚 WordPress Books 🔍 WordPress SEO Books 🌐 Web Hosting Books 🐳 Docker Books 🐧 Linux Books 🐍 Python Books 💰 Affiliate Marketing 💵 Passive Income Books 🖥️ Server Books ☁️ Cloud Computing Books 🚀 DevOps Books ⭐ MiniMax Token Plan 🔍 Cloud Search
← Back to Home