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.
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
LET’S BUILD
Check syntax and ownership
These checks do not print private configuration values.
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
)
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.
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
Check succeeds; stop on an error before continuing.
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.
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;
}
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.
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
Check succeeds; stop on an error before continuing.
LET’S BUILD
Write the temporary HTTP site
Replace the hostname. The only public HTTP content is ACME challenge material; WordPress is not exposed.
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; }
}
Check succeeds; stop on an error before continuing.
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.
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
)
nginx -t passes and challenge body matches acme-path-check exactly.