Illustrative WIUNIX Academy IT engineering workspace with server/network equipment and an instructional board: Install WordPress on Nginx with a privacy-safe FastCGI cache

WIUNIX ACADEMY

Install WordPress on Nginx with a privacy-safe FastCGI cache

Install WordPress, PHP-FPM and MariaDB over HTTPS, then cache only reviewed public pages and prove that sessions and password-protected content stay separate.

Intermediate110 min readUpdated
Make this guide yours

YOUR SYSTEM. YOUR STEPS.

Where are you working?

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

Almalinux 9.8Reviewed 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

THE OUTCOME

What will you have at the end?

A working single-site WordPress installation over HTTPS, with a dedicated PHP identity and a bounded public cache validated through actual response bodies and user isolation.

01HTTPS clientTLS + temporary setup IP gate
02NginxKnown-public GET → bounded cache; all sessions/private routes → bypass
03PHP-FPM + MariaDBDedicated Unix socket, WordPress, local database
Supported operating systems and versions

Reviewed2026-11-05 against official documentation, release hashes and package paths for13 environments. Historical2026-10-20 Ubuntu24.04.4 namespace fixture used WordPress7.1.3/PHP8.3.6/MariaDB10.11.14/Nginx1.24.0 and checked real bodies, sessions, protected content, cache bypass/invalidation and rollback. The current7.1.4/fresh-host guard/RPM/rolling/service-start paths were not executed here. No public ACME/firewall, full OS installation, SQL restore or WooCommerce claim. DEMO images are illustrative.

Before you begin

  • Fresh disposable x86_64 systemd VM with Python3/iproute2 already available, sudo/SSH,2GB RAM and10GB free disk for this small lab; production sizing is separate.
  • Hostname under your control, correct A/AAAA, reachable 80/443, approved ACME email/account and your workstation public IP.
  • Bash/curl test client and staging accounts with private cookie jars. No existing site/database, CDN/proxy or public FPM listener is assumed.
  • Fresh x86_64 VM with systemd, Python3, iproute2 and an existing reviewed firewall policy; no existing database/site/PHP state.
01

LET’S BUILD

Confirm the fresh host and DNS

Use only a disposable fresh x86_64 systemd VM with Python3 and iproute2 already available. This is not an in-place install or migration. The guard rejects pre-existing web/database/PHP/certificate state, service units and unsafe or overlapping paths before package changes. Choose absolute system directories outside home; the form describes path syntax, not permission to overwrite existing state. Review the printed listeners and stop if any other service owns ports80/443/3306.

Set real DNS A/AAAA and the exact administrator source IP. Keep SSH and the existing firewall policy. No CDN/trusted-proxy design is included; public HTTP serves only challenges until TLS setup.

Bash

WordPress server, Bash, existing sudo administrator.

(
set -euo pipefail
. /etc/os-release
test "$ID" = almalinux
test "$VERSION_ID" = 9.8
test "$(uname -m)" = x86_64
test "$(ps -p 1 -o comm=)" = systemd
sudo python3 - <<'PY'
import os,pathlib,stat
paths=['/var/www/wordpress','/var/cache/nginx/wordpress','/etc/nginx','/etc/php','/etc/php-fpm.d','/var/lib/mysql','/etc/letsencrypt','/etc/academy-wordpress','/etc/php-fpm.conf','/etc/mysql','/etc/my.cnf','/etc/my.cnf.d','/var/lib/letsencrypt']
selected=[pathlib.Path(x)for x in paths[:2]]
if any(not x.is_absolute()or '..'in x.parts or x.parts[1:2]in[('home',),('root',)] for x in selected):raise SystemExit('Choose absolute system paths; STOP')
if selected[0]==selected[1] or any(x in y.parents for x,y in (selected,selected[::-1])):raise SystemExit('Paths must be disjoint')
for x in selected:
 for raw in paths[2:]:
  y=pathlib.Path(raw)
  if x==y or x in y.parents or y in x.parents:raise SystemExit('Selected path overlaps reserved state: '+str(x))
for raw in paths:
 p=pathlib.Path(raw)
 if p.exists()or p.is_symlink():raise SystemExit('Existing state: '+raw)
 for a in p.parents:
  try:s=a.lstat()
  except FileNotFoundError:continue
  if stat.S_ISLNK(s.st_mode)or not stat.S_ISDIR(s.st_mode)or s.st_uid!=0 or s.st_mode&0o022:raise SystemExit('Unsafe parent: '+str(a))
PY
if getent passwd wp-php || getent group wp-php; then exit 1; fi
for unit in nginx.service mariadb.service mysql.service php-fpm.service php8.5-fpm.service php8.4-fpm.service php8.3-fpm.service php8.2-fpm.service certbot.service certbot.timer certbot-renew.service certbot-renew.timer; do
  state=$(systemctl show "$unit" -p LoadState --value)
  test "$state" = not-found
done
sudo ss -lntup
getent ahosts blog.example.com
)
How do I know it worked?

Selected OS matches the server, DNS is correct and tutorial paths/ports do not collide with another installation.

02

LET’S BUILD

Install the distribution packages

Install the selected release packages from its signed repositories. Services are masked before installation; only the database starts here after a loopback-only binding file is written. Nginx stays off until its complete new configuration validates. The selected PHP pool later starts independently. This fresh-host procedure must not be reused on an existing service.

Bash

WordPress server, Bash, existing sudo administrator.

(
set -euo pipefail
. /etc/os-release
test "$ID" = 'almalinux'
test "$VERSION_ID" = '9.8'
sudo systemctl mask nginx.service mariadb.service php-fpm.service
sudo dnf install -y dnf-plugins-core epel-release
sudo dnf config-manager --set-enabled crb
sudo dnf module enable -y php:8.3 mariadb:10.11
curl_pkg=curl-minimal
if rpm -q curl >/dev/null 2>&1; then curl_pkg=curl; fi
sudo dnf install -y nginx mariadb-server php-fpm php-cli php-mysqlnd php-gd php-mbstring php-xml php-intl php-pecl-zip "$curl_pkg" ca-certificates tar rsync openssl nano certbot python3 shadow-utils policycoreutils-python-utils audit
sudo test ! -e /etc/my.cnf.d/zz-academy-wordpress.cnf
sudo test ! -L /etc/my.cnf.d/zz-academy-wordpress.cnf
printf '[mysqld]\nbind-address=127.0.0.1\n' | sudo tee /etc/my.cnf.d/zz-academy-wordpress.cnf >/dev/null
sudo systemctl unmask mariadb.service
sudo systemctl enable --now mariadb.service
php -v
mariadb --version
nginx -v
rpm -q nginx mariadb-server php-fpm certbot
sudo ss -lntp 'sport = :3306'
)
How do I know it worked?

APT signature/dependency checks pass; record printed versions. No public 3306 listener.

03

LET’S BUILD

Preserve the unused vendor configuration before editing

Only on this freshly installed, still-masked Nginx: preserve its package configuration under a new name, then replace nginx.conf with the next complete file. No unrelated site is carried over. Do not overwrite an existing backup.

Bash

Fresh dedicated Linux VM, existing sudo administrator.

(
set -euo pipefail
sudo test ! -e /etc/nginx/nginx.conf.academy-before
sudo test ! -L /etc/nginx/nginx.conf.academy-before
sudo cp -a /etc/nginx/nginx.conf /etc/nginx/nginx.conf.academy-before
sudo install -d -o root -g root -m 0755 /etc/nginx/conf.d
sudo nano /etc/nginx/nginx.conf
)
How do I know it worked?

The check succeeds; stop and diagnose any failure.

04

LET’S BUILD

Use the complete dedicated Nginx configuration

This fresh single-site configuration intentionally replaces the unused package default server and defines the http include. Preserve mime.types and fastcgi_params supplied by the package.

Nginx · /etc/nginx/nginx.conf

Fresh dedicated Linux VM, existing sudo administrator.

user nginx;
worker_processes auto;
pid /run/nginx.pid;
events { worker_connections 1024; }
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    sendfile on;
    keepalive_timeout 65;
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;
    include /etc/nginx/conf.d/*.conf;
}
How do I know it worked?

The check succeeds; stop and diagnose any failure.

Open the local database console

Fresh Ubuntu/Debian MariaDB commonly uses Unix-socket authentication for local root. Open without SQL history and never pass a real password in a shell argument.

Bash

WordPress server, Bash, existing sudo administrator.

sudo env MYSQL_HISTFILE=/dev/null mariadb
How do I know it worked?

Check succeeds; stop on an error before continuing.

05

LET’S BUILD

Create a schema-scoped application user

In the database console replace the password with a unique random 64-character hexadecimal password from your password manager (only 0–9/a–f, safe in both SQL/PHP string literals). The account has only the schema permissions needed to install/update WordPress tables, no global administration, remote login or GRANT OPTION. GRANT takes effect immediately; no FLUSH PRIVILEGES is needed. Keep this password for wp-config. Stop on an existing schema/user collision.

SQL

MariaDB administrator console opened in the previous panel, not Bash.

CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wp_site'@'localhost' IDENTIFIED BY 'REPLACE_WITH_A_LONG_RANDOM_PASSWORD';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX
  ON wordpress.* TO 'wp_site'@'localhost';
SHOW GRANTS FOR 'wp_site'@'localhost';
EXIT;
How do I know it worked?

SHOW GRANTS lists wordpress.* permissions, with no global administrative grant.

Explore all tutorials ↗