KBeezie

There's no place like ::1

Menu
  • Home
  • Start Here
  • Security Series
  • About

Installing WordPress 6.9.4 on Nginx

2026/05/11 in CMS Guides

WordPress powers a staggering share of the web, and its Nginx configuration has been refined over years of real-world abuse. This guide walks through a production setup for WordPress 6.9.4 — the same stack running behind this site — with security blocks, static caching, rate limiting, and PHP-FPM tuning that actually makes a difference under load.

Prerequisites

  • Nginx 1.18+
  • PHP-FPM 8.1+ (WordPress 6.9 requires PHP 7.4+, but 8.1+ is recommended)
  • PHP extensions: mysqli (or mysqlnd), curl, json, mbstring, gd (or imagick), zip
  • APCu and opcache enabled for object caching and PHP acceleration
  • A domain name pointed to your server
  • SSL certificates via Let's Encrypt

Step 1: Download WordPress

# Create the web root
sudo mkdir -p /var/www/mysite.com/public_html

# Download WordPress
cd /tmp
wget https://wordpress.org/wordpress-6.9.4.zip
sudo unzip wordpress-6.9.4.zip -d /var/www/mysite.com/public_html

# Set ownership
sudo chown -R www-data:www-data /var/www/mysite.com/public_html

Step 2: Base Nginx server block

This is the smallest configuration that will get WordPress running. As with any minimal config, it omits security hardening and static caching — the full production version is at the end of the article.

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name mysite.com;

    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    ssl_certificate /etc/letsencrypt/live/mysite.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mysite.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/mysite.com/chain.pem;

    root /var/www/mysite.com/public_html;

    access_log /var/log/nginx/mysite.access.log;
    error_log /var/log/nginx/mysite.error.log;

    client_max_body_size 8M;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
    }
}
  • try_files $uri $uri/ /index.php$is_args$args; — The standard WordPress front controller. Static files and directories are served directly; everything else routes through index.php with query strings preserved. WordPress handles its own permalink routing from there.
  • client_max_body_size 8M; — Match this to your largest expected media upload. Set it at the server-block level, not globally — Nginx buffers request bodies to disk, and a globally large limit combined with many connections can exhaust disk space during an attack.
  • try_files $uri =404; in the PHP block prevents WordPress from executing non-existent PHP files — a request for /uploads/hack.png/index.php gets a 404 before PHP ever sees it.

At this point you can visit https://mysite.com and run the famous five-minute WordPress installer. But don't stop here — the next sections are where the real configuration happens.


Step 3: WordPress-specific security blocks

These rules block the most common WordPress attack vectors. All of them must appear before the general location ~ \.php$ handler — Nginx processes regex locations in order, and the first match wins.

# wp_security.conf — blocks for known WordPress attack surfaces

# Block PHP execution in the uploads directory
location ~* ^/wp-content/uploads/.*\.php$ {
    deny all;
    access_log off;
    log_not_found off;
}

# Block access to sensitive WordPress files
location ~* ^/(wp-config\.php|readme\.html|license\.txt) {
    deny all;
    access_log off;
    log_not_found off;
}

# Block xmlrpc.php (major brute-force and DDoS vector)
location = /xmlrpc.php {
    deny all;
    access_log off;
    log_not_found off;
}

# Force no caching on the admin area
location /wp-admin {
    expires 0;
}

A note on xmlrpc.php: If you use the WordPress mobile app or Jetpack, you need XML-RPC. For everyone else, blocking it eliminates a persistent source of brute-force login attempts and pingback amplification attacks. Nginx dropping the request before PHP sees it saves CPU cycles and PHP-FPM workers that would otherwise spin up for every bot hammering the endpoint.

On sites where WordPress is already installed, also block the setup-config installer:

# wp_installed.conf — block the installer on existing sites
location = /wp-admin/setup-config.php {
    deny all;
    access_log off;
    log_not_found off;
}

  • ← Previous
  • 1
  • 2
  • 3
  • 4
  • Next →
Tags: wordpress, nginx, php-fpm, caching, wp-config, hardening
©2026 KBeezie | Disclaimer | Privacy Notice