Most of the WordPress sites I host on my own servers run on Nginx. It handles high traffic with very little memory, serves static files extremely fast and gives fine control over caching and security rules. But unlike Apache, Nginx doesn’t read .htaccess files. Everything has to be configured in the server’s own configuration files.
That’s where people get stuck. Copying a random configuration from a forum might work, but it can also leave security holes, break permalinks or serve your wp-config.php as plain text. I’ve cleaned up the results of all three.
This article walks through the server block I use as a starting point for WordPress sites, section by section. Adapt paths and versions to your own server, test after every change and keep a backup of your working configuration.
The full server block
Here’s the complete configuration. We’ll go through each part below.
| location ~* /wp-content/uploads/.*\.(php | phtml | phar)$ { deny all; } | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| location ~* \.(sql | bak | old | orig | log | zip | tar | gz)$ { deny all; } | |||
| location ~* ^/(wp-config\.php | readme\.html | license\.txt)$ { deny all; } | ||||||||
| location ~* \.(css | js | jpg | jpeg | png | gif | webp | avif | svg | ico | woff2?)$ { |
Redirect HTTP and www to one canonical address
The first two server blocks make sure every visitor ends up on a single address: HTTPS without www in this example. If you prefer www, swap them around.
Having one canonical address matters for SEO, because search engines treat http, https, www and non-www as different URLs. It also keeps cookies and logins consistent. Make sure the address in WordPress, under Settings, then General, matches what Nginx redirects to, or you’ll create a redirect loop.
Listening and HTTP/2
The listen 443 ssl lines tell Nginx to accept HTTPS connections on IPv4 and IPv6. The http2 on directive enables HTTP/2, which lets browsers load many files over a single connection. In older Nginx versions, HTTP/2 was enabled with a parameter on the listen line instead. Check your version with nginx -v and use the syntax it supports.
Root and index
The root directive points to the folder containing WordPress. I like to keep WordPress in a public subfolder, so logs, backups and configuration files outside it can never be served by accident.
The index directive tells Nginx to use index.php when a directory is requested.
SSL certificates
The certificate paths shown are typical for Let’s Encrypt certificates issued by Certbot. Certbot can also renew certificates automatically. Check that renewal is working with:
You can add TLS tuning, such as protocols and ciphers, in a shared file included by every site. Modern Nginx and OpenSSL defaults are reasonable, but disabling old protocols like TLS 1.0 and 1.1 is good practice.
Upload size
client_max_body_size controls the largest request Nginx accepts. If it’s too small, uploading large images or importing files fails with a 413 error. It needs to agree with PHP’s upload_max_filesize and post_max_size settings, or uploads will still fail at the PHP level.
Logs per site
Separate access and error logs for each site make troubleshooting much easier. When something breaks, the error log is the first place to look. During security investigations, the access log tells you exactly what requests came in and when.
Make sure logs are rotated, usually by logrotate, so they don’t fill the disk.
Security headers
The add_header lines send basic security headers with every response. They tell browsers not to guess content types, not to allow your pages to be framed by other sites and how much referrer information to share.
One Nginx quirk catches many people: add_header directives in a location block replace those in the server block, rather than adding to them. If you add a header inside a location, such as the static files block, the server-level security headers won’t be sent for those requests. Either repeat the headers or put them in a shared include file.
The WordPress permalink rule
This is the most important line for WordPress:
It tells Nginx to serve the requested file if it exists, then try it as a directory, and otherwise pass the request to WordPress’s index.php. That’s what makes pretty permalinks work. If posts return 404 errors on Nginx, this line is usually missing or wrong.
Passing PHP to PHP-FPM
The PHP location block sends .php requests to PHP-FPM. A few details matter:
- try_files $uri =404 returns a 404 if the PHP file doesn’t exist, rather than passing the request to PHP anyway. This prevents a class of attacks where crafted URLs trick PHP into executing uploaded files.
- SCRIPT_FILENAME tells PHP which file to run.
- fastcgi_pass points to the PHP-FPM socket. I use a separate PHP-FPM pool for each site, running as its own user, so a compromise of one site doesn’t automatically give access to others.
- fastcgi_read_timeout controls how long Nginx waits for PHP. Raise it if legitimate long operations, like imports, time out, but don’t set it absurdly high.
Blocking PHP in uploads
This rule prevents any PHP file in the uploads folder from running:
| <code>location ~* /wp-content/uploads/.*\.(php | phtml | phar)$ { deny all; }</code> |
|---|
Because Nginx checks regular expression locations in order, this rule must come before the general PHP location, or the PHP block will match first and the file will execute. That ordering mistake is common and dangerous. Test it by placing a harmless test.php in uploads and confirming you get a 403.
Blocking XML-RPC
If you don’t use XML-RPC, blocking it stops a common brute force method and saves resources. Remove this rule if you rely on services that need it.
Hiding sensitive files
The remaining deny rules block:
- Hidden files and folders like .git, .env and .htaccess, while still allowing .well-known for SSL verification.
- Backup and archive files that should never be downloadable.
- wp-config.php, readme.html and license.txt. PHP files normally execute rather than display, but if PHP ever breaks, Nginx could serve wp-config.php as plain text, exposing your database password. Denying it outright prevents that.
Caching static files
The final block tells browsers to cache images, styles, scripts and fonts for 30 days, and turns off access logging for them to reduce log noise. WordPress adds version numbers to asset URLs, so updated files are still fetched when they change.
Adding page caching
For high-traffic sites, Nginx can also cache full pages with FastCGI caching, serving cached HTML without running PHP at all. It’s very fast, but it needs careful exclusions for logged-in users, WooCommerce carts and checkouts. I cover that in a separate guide on caching. If your site uses a caching plugin that writes static files, the plugin’s documentation usually includes Nginx rules for serving them directly.
Testing and reloading
Always test the configuration before reloading:
nginx -t catches syntax errors. Reload, rather than restart, applies the new configuration without dropping active connections. After reloading, test the site: homepage, a post, the admin, a form submission and, for stores, the checkout.
Keep your configuration organised
As you add sites to a server, configuration files multiply. A little structure saves a lot of confusion later. I keep shared rules, such as security headers, static file caching and the uploads PHP block, in separate snippet files under a folder like /etc/nginx/snippets, then include them in each server block with a single line. Fixing a rule once then fixes it everywhere.
I also keep the whole Nginx configuration directory in Git. Every change gets a short commit message explaining why it was made. When something breaks after a change, I can see exactly what changed and roll it back in seconds, rather than trying to remember which line I edited three weeks ago.
Finally, document anything unusual about a site’s configuration in a comment at the top of its server block. Future you, or the next administrator, will be grateful for a note explaining why a particular redirect or exception exists.
Frequently asked questions
Can I use .htaccess rules from plugins on Nginx?
Not directly. Nginx ignores .htaccess files. Many plugins provide equivalent Nginx rules in their documentation, which you add to your server block. Some hosts using Nginx with Apache behind it do support .htaccess, so check your stack.
Why does my WordPress admin redirect in a loop?
Usually because the WordPress site URL doesn’t match the canonical address Nginx redirects to, or because HTTPS isn’t detected correctly behind a proxy or load balancer. Check Settings, then General, and make sure both addresses match your Nginx redirect.
Do I need separate PHP-FPM pools per site?
It’s not strictly required, but it’s good practice on servers hosting several sites. Separate pools with separate users isolate sites from each other and let you tune resources for each one.
Is this configuration enough for security?
It covers important server-level protections, but it’s one layer. Keep WordPress and plugins updated, use strong authentication, monitor files and keep backups.
Final thoughts
A good Nginx configuration makes WordPress faster and safer before a single plugin is installed. The essentials are a single canonical address, the try_files permalink rule, careful PHP handling with PHP blocked in uploads, protection for sensitive files and sensible caching headers. Understand each line rather than copying blindly, test every change and keep your working configuration under version control. Your server will reward you with speed and fewer surprises.




