WIUNIX ACADEMY
Install WordPress on Nginx with a privacy-safe FastCGI cache
Page 6 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
Separate rewritten page keys and request methods
The key contains scheme, request method, validated host, ORIGINAL request URI and a generation. Permalink rewrites must not collapse all pages onto index.php. Only GET is cache-eligible. HEAD is deliberately bypassed too, avoiding a header-only response populating a GET key. The empty anonymous POST to this staging public page has no form action and must bypass. Nginx skips its cache engine for POST, so X-WP-Cache can be absent rather than BYPASS; it must never be a cached HIT/STALE response.
Allowed staging client, Bash/curl; all cookie files are private and local to this client.
(
set -euo pipefail
work=$(mktemp -d)
code=$(curl -sS --connect-timeout 5 --max-time 20 -D "$work/home.h" -o "$work/home.html" -w '%{http_code}' https://blog.example.com/)
[ "$code" = 200 ]
grep -F PUBLIC_HOME_MARKER "$work/home.html"
if grep -Fq PUBLIC_CACHE_MARKER "$work/home.html"; then
echo 'Unexpected cached/private content or status' >&2; exit 1
else
rc=$?; [ "$rc" = 1 ] || exit "$rc"
fi
code=$(curl -sS --connect-timeout 5 --max-time 20 -X POST -d '' -D "$work/post.h" -o "$work/post.html" -w '%{http_code}' https://blog.example.com/sample-page/)
[ "$code" = 200 ]
if grep -Eiq '^X-WP-Cache: (HIT|STALE|UPDATING|REVALIDATED)[[:space:]]*$' "$work/post.h"; then
echo 'Unexpected cached/private content or status' >&2; exit 1
else
rc=$?; [ "$rc" = 1 ] || exit "$rc"
fi
grep -F PUBLIC_CACHE_MARKER "$work/post.html"
curl -sS --head --connect-timeout 5 --max-time 20 https://blog.example.com/sample-page/
)
Home/Sample keys differ; POST has correct public body and is never served from cache; HEAD bypasses with no body.
LET’S BUILD
Prove password-protected body isolation
Use locked-test, absent from the allowlist. In the protected browser enter its test password and privately export the real post-password jar. Anonymous must see the form without secret text, valid password session must see secret text, and new anonymous must still lack it. Test actual cart/checkout/account/plugin workflows with distinct real users before broadening this configuration. This guide does not install WooCommerce or prove third-party plugin behavior.
Allowed staging client, Bash/curl; all cookie files are private and local to this client.
(
set -euo pipefail
umask 077
work=$(mktemp -d)
base=https://blog.example.com
code=$(curl -sS --connect-timeout 5 --max-time 20 -D "$work/locked.h" -o "$work/locked.html" -w '%{http_code}' "$base/locked-test/")
[ "$code" = 200 ]
grep -Eiq '^X-WP-Cache: BYPASS[[:space:]]*$' "$work/locked.h"
grep -F 'post-password-form' "$work/locked.html"
if grep -Fq LOCKED_CONTENT_MARKER "$work/locked.html"; then
echo 'Unexpected cached/private content or status' >&2; exit 1
else
rc=$?; [ "$rc" = 1 ] || exit "$rc"
fi
# Existing private jar from entering this test post password in your browser.
jar=$HOME/wp-staging-postpass.cookies
[ -s "$jar" ]
chmod 0600 "$jar"
code=$(curl -sS --connect-timeout 5 --max-time 20 -b "$jar" -D "$work/unlocked.h" -o "$work/unlocked.html" -w '%{http_code}' "$base/locked-test/")
[ "$code" = 200 ]
grep -Eiq '^X-WP-Cache: BYPASS[[:space:]]*$' "$work/unlocked.h"
grep -F LOCKED_CONTENT_MARKER "$work/unlocked.html"
code=$(curl -sS --connect-timeout 5 --max-time 20 -D "$work/again.h" -o "$work/again.html" -w '%{http_code}' "$base/locked-test/")
[ "$code" = 200 ]
if grep -Fq LOCKED_CONTENT_MARKER "$work/again.html"; then
echo 'Unexpected cached/private content or status' >&2; exit 1
else
rc=$?; [ "$rc" = 1 ] || exit "$rc"
fi
)
All requests BYPASS; only valid post-password session sees LOCKED_CONTENT_MARKER.
LET’S BUILD
Open the completed site to visitors
After setup and staging privacy tests pass, use this editor to remove ONLY the temporary allow/deny lines at HTTPS server scope. Keep location-specific configuration/hidden-file protection and all cache bypass rules. Run the earlier validation/reload panel, then require public 200 and working login from an independent connection. Keep outer CDN caching disabled during origin tests.
WordPress server, Bash, existing sudo administrator.
sudo nano /etc/nginx/sites-available/wordpress
Finished HTTPS site is public; sessions and private URLs remain uncached.
Prepare the renewal deploy hook
Keep HTTP challenge routing. The package’s existing Certbot timer owns renewal; do not create a competing scheduler. The following hook validates Nginx before reloading a renewed certificate.
WordPress server, Bash, existing sudo administrator.
sudo install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
sudo nano /etc/letsencrypt/renewal-hooks/deploy/20-nginx-validated
Check succeeds; stop on an error before continuing.
Validate renewed certificate deployment
Save as file contents; do not paste a script body into an unrelated shell session.
File contents; save in the named file, not the shell.
#!/bin/sh
set -eu
/usr/sbin/nginx -t
/bin/systemctl reload nginx
Check succeeds; stop on an error before continuing.
LET’S BUILD
Check renewal and its scheduler
The direct hook call gracefully reloads only after validation. Dry-run uses ACME staging and may take time; deploy hooks are not normally run by dry-run, so the direct call separately tests the hook.
WordPress server, Bash, existing sudo administrator.
(
set -euo pipefail
sudo chmod 0750 /etc/letsencrypt/renewal-hooks/deploy/20-nginx-validated
sudo /etc/letsencrypt/renewal-hooks/deploy/20-nginx-validated
sudo certbot renew --dry-run
systemctl list-timers --all | grep certbot
)
Hook and dry-run succeed; existing package timer is scheduled.