← Start of tutorial

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.

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
23

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.

Bash

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/
)
How do I know it worked?

Home/Sample keys differ; POST has correct public body and is never served from cache; HEAD bypasses with no body.

24

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.

Bash

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
)
How do I know it worked?

All requests BYPASS; only valid post-password session sees LOCKED_CONTENT_MARKER.

25

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.

Bash

WordPress server, Bash, existing sudo administrator.

sudo nano /etc/nginx/sites-available/wordpress
How do I know it worked?

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.

Bash

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
How do I know it worked?

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.

Shell · /etc/letsencrypt/renewal-hooks/deploy/20-nginx-validated

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

#!/bin/sh
set -eu
/usr/sbin/nginx -t
/bin/systemctl reload nginx
How do I know it worked?

Check succeeds; stop on an error before continuing.

26

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.

Bash

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
)
How do I know it worked?

Hook and dry-run succeed; existing package timer is scheduled.

Explore all tutorials ↗