In the previous chapter we set up server monitoring and discussed ongoing maintenance for our Ubuntu web server. In this final chapter I offer a complete Nginx configuration optimized to configure WordPress websites.
In addition to amalgamating all information from the previous 9 chapters, I will be drawing upon best practices from my experience and various sources I’ve come across over the years. The following example domains are included, each demonstrating a different scenario:
single-site.com– WordPress on HTTPSsingle-site-with-caching.com– WordPress on HTTPS with FastCGI page cachingmultisite-subdomain.com– WordPress Multisite using subdomainsmultisite-subdirectory.com– WordPress Multisite using subdirectories
Before diving into this configuration, we recommend double-checking that you have the latest version of MySQL by referencing the tutorial in chapter 2 of this guide. Once that’s confirmed, you’ll see that the configuration files contain inline documentation throughout and are structured in a way to reduce duplicate directives, which are common across multiple WordPress configurations. This should allow you to quickly create new WordPress sites with sensible defaults out of the box, which can be customized as required.
Usage
You can use these configs as a reference for creating your own configuration, or directly by copying into your etc directory. Follow the steps below to replace your existing Nginx server configuration.
Back up any existing config with the following command:
sudo mv /etc/nginx /etc/nginx.backup
Copy one of the example configurations from sites-available to sites-available/yourdomain.com:
sudo cp /etc/nginx/sites-available/single-site.com /etc/nginx/sites-available/yourdomain.com
Edit the config as necessary, paying close attention to the server name and server paths. You will also need to create any directories used within the configuration and configure Nginx to have read/write permissions.
To enable the site, symlink the configuration into the sites-enabled directory:
sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/yourdomain.com
Test the configuration:
sudo nginx -t
If the configuration passes, reload Nginx:
sudo systemctl reload nginx.service
Nginx Config Preview
The following is a preview of the single-site.com Nginx configuration file that’s contained in the package. It should give you a good idea of what it’s like to use our configs.
server {
# Ports to listen on
listen 443 ssl;
listen [::]:443 ssl;
listen 443 quic;
listen [::]:443 quic;
http2 on;
# Server name to listen for
server_name single-site.com;
# Path to document root
root /sites/single-site.com/public;
# Paths to certificate files.
ssl_certificate /etc/letsencrypt/live/single-site.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/single-site.com/privkey.pem;
# File to be used as index
index index.html index.php;
# Overrides logs defined in nginx.conf, allows per site logs.
access_log /sites/single-site.com/logs/access.log;
error_log /sites/single-site.com/logs/error.log;
# Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
# Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
location ~* /\.(?!well-known\/) {
deny all;
}
# Prevent access to certain file extensions
location ~\.(ini|log|conf)$ {
deny all;
}
# Deny access to any files with a .php extension in the uploads directory
# Works in sub-directory installs and also in multisite network
# Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
}
# Hide Nginx version in error messages and reponse headers.
server_tokens off;
# Don't allow pages to be rendered in an iframe on external domains.
add_header X-Frame-Options "SAMEORIGIN" always;
# MIME sniffing prevention
add_header X-Content-Type-Options "nosniff" always;
# The X-XSS-Protection header has been deprecated by modern browsers and its use can introduce additional security issues on the client side.
# As such, it is recommended to set the header as X-XSS-Protection: 0 in order to disable the XSS Auditor, and not allow it to take the default behavior of the browser handling the response.
# Please use Content-Security-Policy instead.
add_header X-XSS-Protection "0" always;
# Whitelist sources which are allowed to load assets (JS, CSS, etc). The following will block
# only none HTTPS assets, but check out https://scotthelme.co.uk/content-security-policy-an-introduction/
# for an in-depth guide on creating a more restrictive policy.
# add_header Content-Security-Policy "default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval';" always;
# Don't cache appcache, document html and data.
location ~* \.(?:manifest|appcache|html?|xml|json)$ {
expires 0;
}
# Cache RSS and Atom feeds.
location ~* \.(?:rss|atom)$ {
expires 1h;
}
# Caches images, icons, video, audio, HTC, etc.
location ~* \.(?:jpg|jpeg|gif|png|avif|webp|ico|cur|gz|svg|mp4|mp3|ogg|ogv|webm|htc)$ {
expires 1y;
access_log off;
}
# Cache svgz files, but don't compress them.
location ~* \.svgz$ {
expires 1y;
access_log off;
gzip off;
}
# Cache CSS and JavaScript.
location ~* \.(?:css|js)$ {
expires 1y;
access_log off;
}
# Cache WebFonts.
location ~* \.(?:ttf|ttc|otf|eot|woff|woff2)$ {
expires 1y;
access_log off;
add_header Access-Control-Allow-Origin *;
}
# Don't record access/error logs for robots.txt.
location = /robots.txt {
try_files $uri $uri/ /index.php$is_args$args;
access_log off;
log_not_found off;
}
# Don't use outdated SSLv3 protocol. Protects against BEAST and POODLE attacks.
ssl_protocols TLSv1.2 TLSv1.3;
# Use secure ciphers
ssl_ecdh_curve X25519:prime256v1:secp384r1;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_dhparam /etc/nginx/dhparam;
ssl_prefer_server_ciphers off;
ssl_session_tickets off;
# Define the size of the SSL session cache in MBs.
ssl_session_cache shared:SSL:10m;
# Define the time in minutes to cache SSL sessions.
ssl_session_timeout 1h;
# Use HTTPS exclusively for 1 year, uncomment one. Second line applies to subdomains.
add_header Strict-Transport-Security "max-age=31536000;";
# add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
# Advertises support for HTTP/3
add_header Alt-Svc 'h3=":443"; ma=86400';
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
try_files $uri =404;
include global/fastcgi-params.conf;
# Use the php pool defined in the upstream variable.
# See global/php-pool.conf for definition.
fastcgi_pass $upstream;
}
}
# Redirect http to https
server {
listen 80;
listen [::]:80;
server_name single-site.com www.single-site.com;
return 301 https://single-site.com$request_uri;
}
# Redirect www to non-www
server {
listen 443 ssl;
listen [::]:443 ssl;
listen 443 quic;
listen [::]:443 quic;
http2 on;
server_name www.single-site.com;
# Advertises support for HTTP/3
add_header Alt-Svc 'h3=":443"; ma=86400';
return 301 https://single-site.com$request_uri;
}
That’s All Folks!
Job done! I encourage you to explore the config files further and read through the documented configuration to get a feel for what’s going on. It should feel familiar as it follows the same conventions used throughout this guide.
Over time I will improve the configuration and add new best practices as they emerge. If you have any improvements, please let me know.
That concludes this chapter and the guide as a whole. It’s been quite a journey, but hopefully you’ve learned a lot and are more confident managing a server than when you started.