Skip to content
ohsudo, Home
Networking

Nginx for WordPress with PHP-FPM: a complete, safe config

Run WordPress on Nginx and PHP-FPM: a complete HTTPS config, pretty permalinks, blocking PHP in uploads, upload limits, caching and the common errors, step by step.

Intermediate3 min readApplies to Nginx 1.18+ · PHP 8.1+ · WordPress 6+ · Ubuntu · Debian

by OhSudo ·

WordPress often comes with instructions for Apache, and its .htaccess file does nothing on Nginx. So permalinks and the security rules have to be in Nginx’s own config.

The Nginx Config Generator with these options writes the HTTPS and redirect parts for your domain. It doesn’t write PHP, so add the location blocks below.

The config

server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
root /var/www/example.com;
index index.php;
client_max_body_size 64m;
# Pretty permalinks: anything that isn't a file goes to WordPress.
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
# An uploaded .php file must never run.
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
}
# .htaccess, .git, .env and other hidden files.
location ~ /\.(?!well-known) {
deny all;
}
location = /wp-config.php {
deny all;
}
location ~* \.(?:css|js|jpe?g|png|gif|webp|svg|ico|woff2?)$ {
expires 30d;
access_log off;
}
}

1. Install PHP and the extensions

Terminal window
sudo apt install nginx php-fpm php-mysql php-curl php-gd php-intl php-mbstring php-xml php-zip php-imagick
php -v
systemctl list-units 'php*-fpm*' --no-pager
ls /run/php/

The number in the socket’s name has to match your PHP version, so change php8.3-fpm.sock in the config if yours differs. That’s a classic reason for a 502.

snippets/fastcgi-php.conf comes with Nginx on Debian and Ubuntu. On RHEL, the PHP package adds /etc/nginx/default.d/php.conf, and the socket is /run/php-fpm/www.sock.

2. Set the limits together

Nginx and PHP each limit uploads, and the lowest wins. So set all of them in /etc/php/8.3/fpm/php.ini:

upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
max_execution_time = 120
Terminal window
sudo systemctl restart php8.3-fpm

An upload over the limit fails with 413 Request Entity Too Large, or in WordPress with a message about exceeding upload_max_filesize.

3. Permissions

WordPress runs as www-data, and only needs to write in wp-content:

Terminal window
sudo chown -R root:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 750 {} +
sudo find /var/www/example.com -type f -exec chmod 640 {} +
sudo chown -R www-data:www-data /var/www/example.com/wp-content

This stops a hacked plugin from rewriting WordPress itself. Automatic core updates can’t write there either, so either update with WP-CLI, or give www-data the whole folder and accept that risk. The chmod Calculator explains these numbers. chmod 777 is never the answer.

4. Get HTTPS, and turn it on

Terminal window
sudo certbot certonly --standalone -d example.com -d www.example.com \
--pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

If that fails, see Certbot failed to authenticate. Then set the address in WordPress, under Settings → General, or in wp-config.php:

define('WP_HOME', 'https://example.com');
define('WP_SITEURL', 'https://example.com');

5. Behind Cloudflare or a load balancer

WordPress can’t see that the visitor used HTTPS, so it redirects forever, and shows ERR_TOO_MANY_REDIRECTS. Add this near the top of wp-config.php:

if (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') {
$_SERVER['HTTPS'] = 'on';
}

Also read Too many redirects, since Cloudflare’s SSL mode matters too.

6. Block what you don’t use

If you don’t use the mobile app or Jetpack, XML-RPC is mostly a target for password guessing. Add this to the server block:

location = /xmlrpc.php {
deny all;
}

When it goes wrong

You see Means Go to
404 on every page except the home page Missing try_files, as .htaccess doesn’t apply The config above
502 Bad Gateway Wrong PHP socket, or PHP-FPM stopped Nginx 502
504 Gateway Time-out A slow plugin or import Nginx 504
403 Forbidden No index.php in root, or wrong permissions Nginx 403
A .php file downloads, rather than runs The location ~ \.php$ block is missing The config above
Error establishing a database connection MySQL is down or refuses you MySQL 2003, MySQL 1045

Warning

A location ~ \.php$ that only passes files along can run anything named .php, so the block on uploads is essential. Without it, one bad upload in a plugin gives an attacker a way to run code on your server. Keep the two deny rules, and keep WordPress and its plugins updated.

Found this useful? Share it.

Found a mistake? Let us know at contact@ohsudo.com.

Tools

Try it yourself

Articles

Related articles