← Start of tutorial

WIUNIX ACADEMY

Install WordPress on Nginx with a privacy-safe FastCGI cache

Page 3 of 8. Review the earlier steps and prerequisites before continuing.

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

Ubuntu 26.10Reviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment4 editable values
09

LET’S BUILD

Check syntax and ownership

These checks do not print private configuration values.

Bash

WordPress server, Bash, existing sudo administrator.

(
set -euo pipefail
sudo php8.5 -l /var/www/wordpress/wp-config.php
if sudo grep -q REPLACE_ /var/www/wordpress/wp-config.php; then
  echo 'Replace all configuration placeholders first' >&2; exit 1
fi
sudo -u wp-php test -r /var/www/wordpress/wp-config.php
sudo -u wp-php test ! -w /var/www/wordpress/wp-config.php
sudo -u wp-php test -w /var/www/wordpress/wp-content/uploads
)
How do I know it worked?

PHP reads but cannot write config; uploads is writable; no placeholders remain.

Prepare the cache directory and policy

The worker owns the cache directory. Stock Debian/Ubuntu includes conf.d inside http; the later full configuration test verifies this assumption. map cannot live in server.

Bash

WordPress server, Bash, existing sudo administrator.

sudo install -d -o www-data -g www-data -m 0700 /var/cache/nginx/wordpress
sudo nano /etc/nginx/conf.d/wordpress-cache.conf
How do I know it worked?

Check succeeds; stop on an error before continuing.

10

LET’S BUILD

Opt in only known public pages

Only homepage and sample-page are eligible. All other paths, including admin/login, preview, API/XML-RPC, cron, cart/checkout/account, feeds, searches and password-protected pages, stay outside the allowlist. Any cookie, Authorization, query string, non-GET method or percent-encoded path bypasses both lookup and storage. This covers WordPress login/post-password/comment cookies and unknown plugin sessions without maintaining an incomplete cookie-name list. Remove an allowed page and rotate the key BEFORE protecting or personalizing it. Never add geo/device/private/plugin-personalized content without separately assessing keys and response controls. UTM parameters also bypass deliberately; ignoring them requires application review.

Nginx · /etc/nginx/conf.d/wordpress-cache.conf

File contents; save in the named file, not the shell.

# http context: included by the stock Debian/Ubuntu nginx.conf
fastcgi_cache_path /var/cache/nginx/wordpress levels=1:2 keys_zone=WORDPRESS:20m
    max_size=512m inactive=10m use_temp_path=off;
map $request_method $wp_skip_method { default 1; GET 0; }
# Every cookie is private until separately assessed, including WordPress,
# wp-postpass, comment_author, WooCommerce and plugin session cookies.
map $http_cookie $wp_skip_cookie { default 1; "" 0; }
map $http_authorization $wp_skip_auth { default 1; "" 0; }
map $args $wp_skip_args { default 1; "" 0; }
# Opt in ONLY known public HTML URLs. Everything else stays dynamic.
map $request_uri $wp_skip_route {
    default 1;
    "/" 0;
    "/sample-page/" 0;
}
# Defense in depth for the standard private/dynamic endpoints.
map $request_uri $wp_private_path {
    default 0;
    ~*^/(wp-admin|wp-login\.php|wp-cron\.php|wp-json|xmlrpc\.php|cart|checkout|my-account|account)(/|\?|$) 1;
    ~*% 1;
}
How do I know it worked?

512 MB bounded cache and 20 MB keys zone declared; virtual host caching is still off.

Prepare challenge-only HTTP

Port 80 must be reachable by the certificate authority. If a certificate is already managed for this domain, reuse that owner’s approved automation and paths rather than create competing issuance.

Bash

WordPress server, Bash, existing sudo administrator.

sudo install -d -o root -g root -m 0755 /var/lib/letsencrypt/.well-known/acme-challenge
sudo nano /etc/nginx/sites-available/wordpress
How do I know it worked?

Check succeeds; stop on an error before continuing.

11

LET’S BUILD

Write the temporary HTTP site

Replace the hostname. The only public HTTP content is ACME challenge material; WordPress is not exposed.

Nginx · /etc/nginx/sites-available/wordpress

File contents; save in the named file, not the shell.

server {
    listen 80;
    listen [::]:80;
    server_name blog.example.com;
    location ^~ /.well-known/acme-challenge/ {
        root /var/lib/letsencrypt;
        default_type text/plain;
        try_files $uri =404;
    }
    location / { return 404; }
}
How do I know it worked?

Check succeeds; stop on an error before continuing.

12

LET’S BUILD

Validate and enable the challenge endpoint

Stop on an existing symlink collision. Reload only after full Nginx validation. Verify the same challenge URL from an outside client too; local reachability is insufficient evidence for the CA.

Bash

WordPress server, Bash, existing sudo administrator.

(
set -euo pipefail
sudo test ! -e /etc/nginx/sites-enabled/wordpress
sudo ln -s /etc/nginx/sites-available/wordpress /etc/nginx/sites-enabled/wordpress
sudo nginx -t
sudo systemctl reload nginx
printf 'acme-path-check\n' | sudo tee /var/lib/letsencrypt/.well-known/acme-challenge/academy-check >/dev/null
curl -fsS --connect-timeout 5 --max-time 15 http://blog.example.com/.well-known/acme-challenge/academy-check | grep -Fx acme-path-check
)
How do I know it worked?

nginx -t passes and challenge body matches acme-path-check exactly.

Explore all tutorials ↗