This guide installs a production-oriented WordPress site on Ubuntu with Nginx, PHP-FPM, and Oracle MySQL. It covers DNS, firewall rules, a dedicated PHP-FPM identity, HTTPS, database credentials, file permissions, verification, and routine maintenance. The web process can update WordPress content, but it cannot rewrite the WordPress core.
Architecture and Trust Boundaries
- Nginx accepts public HTTP and HTTPS connections and serves static files.
- PHP-FPM runs WordPress as the unprivileged wordpress account in an isolated pool.
- MySQL listens locally; WordPress connects using a dedicated database account.
- Certbot obtains and renews the HTTPS certificate.
- Ubuntu packages provide PHP and system updates; Oracle's signed APT repository provides MySQL 8.4 LTS.
The PHP process can write to wp-content, which WordPress needs for uploads and dashboard-managed plugins and themes. The core files and wp-config.php remain owned by root. A compromised plugin therefore cannot silently replace the application bootstrap or core files. WordPress core updates are deployed separately in the maintenance section below.
Prerequisites
Use a fresh Ubuntu Server 26.04 LTS host with:
- A sudo-enabled account and SSH access. Keep the current SSH session open while changing firewall rules.
- A public IPv4 address and a domain name whose DNS you control.
- At least 2 GB of RAM for a small site; media, plugins, and traffic may require more.
- Inbound TCP ports 80 and 443 allowed by both the cloud firewall and the host firewall.
Create an A record for the site name, for example:
example.org. 3600 IN A 203.0.113.10
www.example.org. 3600 IN A 203.0.113.10 Replace example.org and 203.0.113.10 throughout. Publish AAAA records only when the server is reachable over IPv6. Check both names before requesting a certificate:
dig +short A example.org
dig +short A www.example.org Update Ubuntu and Configure the Firewall
Connect over SSH and install current security updates:
sudo apt update sudo apt full-upgrade
Permit SSH before enabling UFW so the remote session remains reachable. Add the web ports, then enable the firewall:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status verbose Do not open TCP port 3306 to the Internet. If a cloud firewall is enabled, allow SSH only from trusted administrator addresses where practical, plus public TCP ports 80 and 443.
Install Nginx and PHP
Install Nginx, PHP 8.5-FPM, the database driver, and the extensions used by WordPress and common image workflows:
sudo apt install nginx ca-certificates curl gnupg rsync unzip \
certbot python3-certbot-nginx \
php8.5-cli php8.5-fpm php8.5-mysql php8.5-curl php8.5-gd \
php8.5-mbstring php8.5-xml php8.5-zip php8.5-intl \
php8.5-bcmath php8.5-opcache Verify PHP and the required extensions are available:
php8.5 -v
php8.5 -m | grep -E 'curl|dom|gd|intl|mbstring|mysqli|PDO|xml|zip' Keep the package versions selected by Ubuntu's signed repositories. Do not add a third-party PHP PPA just to obtain a point release; install security updates with APT as they become available.
Install MySQL 8.4 LTS
Ubuntu's archive provides a security-maintained MySQL 8.4 package. To install the latest upstream MySQL 8.4 LTS packages on supported amd64 servers, use Oracle's signed APT repository. The repository currently publishes MySQL 8.4.13 for Ubuntu 26.04. If this host already runs MySQL, MariaDB, or Percona, stop here and plan a database backup and migration first; do not switch repositories on an existing database casually.
Download the current MySQL APT configuration package from Oracle's official download page. The current package is shown here:
cd /tmp
curl -fLO https://dev.mysql.com/get/mysql-apt-config_0.8.40-1_all.deb
sudo dpkg -i mysql-apt-config_0.8.40-1_all.deb In the configuration menu, select MySQL Server & Cluster, choose the mysql-8.4-lts series, and confirm the selection. If Oracle has published a newer configuration package, use its current filename from the official download page instead of guessing a repository codename. APT verifies signed repository metadata when it downloads the server packages.
sudo apt update apt-cache policy mysql-server sudo apt install mysql-server
If the installer asks for a MySQL root password, use a unique secret and store it safely. If you choose Unix-socket authentication, administer MySQL through sudo mysql; otherwise use mysql -u root -p. Confirm the installed server and its local listener:
mysql --version
sudo systemctl status mysql --no-pager
sudo ss -ltnp | grep ':3306' The database listener should be local, not bound to a public interface. If the Oracle repository does not publish packages for the server's CPU architecture, use Ubuntu's supported mysql-server package instead; keep it on the MySQL 8.4 LTS series and continue applying Ubuntu security updates.
Create a Dedicated WordPress Database
Generate a unique database password and keep it available for the configuration step:
openssl rand -hex 32 Open the local administrative MySQL shell:
sudo mysql
If root uses password authentication, connect with mysql -u root -p instead. In the MySQL prompt, create a database and a user restricted to local connections:
CREATE DATABASE wordpress
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
CREATE USER 'wp_site'@'localhost'
IDENTIFIED BY 'REPLACE_WITH_THE_GENERATED_PASSWORD';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wp_site'@'localhost';
EXIT; Replace the placeholder with the generated value. Do not use the MySQL root account in WordPress, reuse an SSH password, or expose database credentials in a public repository.
Create an Isolated PHP-FPM Pool
Create a system account with no interactive login:
sudo adduser --system --group --no-create-home \ --shell /usr/sbin/nologin wordpress
Create /etc/php/8.5/fpm/pool.d/wordpress.conf:
[wordpress]
user = wordpress
group = www-data
listen = /run/php/wordpress-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = ondemand
pm.max_children = 8
pm.process_idle_timeout = 10s
pm.max_requests = 500
security.limit_extensions = .php pm.max_children is a starting point, not a universal capacity setting. Measure PHP worker memory and tune it to the server's available RAM. Restart PHP-FPM and confirm that the pool loaded:
sudo systemctl restart php8.5-fpm
sudo systemctl status php8.5-fpm --no-pager
sudo test -S /run/php/wordpress-fpm.sock Download WordPress and Set File Permissions
Download the current stable release over HTTPS and unpack it into a root-owned document directory:
cd /tmp
curl -fSLO https://wordpress.org/latest.tar.gz
sudo install -d -o root -g www-data -m 0750 /var/www/wordpress
sudo tar -xzf latest.tar.gz --strip-components=1 -C /var/www/wordpress
sudo chown -R root:www-data /var/www/wordpress
sudo find /var/www/wordpress -type d -exec chmod 0750 {} +
sudo find /var/www/wordpress -type f -exec chmod 0640 {} + Give the PHP-FPM account write access only to wp-content. Nginx can still read these files through the www-data group, but the web process cannot modify the WordPress core:
sudo chown -R wordpress:www-data /var/www/wordpress/wp-content
sudo find /var/www/wordpress/wp-content -type d -exec chmod 2750 {} +
sudo find /var/www/wordpress/wp-content -type f -exec chmod 0640 {} + These permissions allow dashboard-managed plugins, themes, and uploads. The Nginx configuration below explicitly blocks PHP execution from the uploads directory. Do not replace these settings with world-writable permissions such as chmod -R 777.
Configure Nginx
Create /etc/nginx/sites-available/wordpress, replacing both example host names:
server {
listen 80;
listen [::]:80;
server_name example.org www.example.org;
root /var/www/wordpress;
index index.php;
client_max_body_size 32m;
location / {
try_files $uri $uri/ /index.php?$args;
}
location = /wp-config.php {
deny all;
}
location ~* ^/wp-content/uploads/.*\.php$ {
deny all;
}
location ~ /\.(?!well-known).* {
deny all;
}
location ~ \.php$ {
try_files $uri =404;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/wordpress-fpm.sock;
}
} Enable the site. On a fresh Nginx installation, remove the default site symlink if it is still enabled:
sudo ln -s /etc/nginx/sites-available/wordpress \
/etc/nginx/sites-enabled/wordpress
sudo unlink /etc/nginx/sites-enabled/default Skip the unlink command if the default symlink is already absent. Validate the configuration before reloading Nginx:
sudo nginx -t sudo systemctl reload nginx
Enable HTTPS
Wait until the domain's A and any published AAAA records reach this server. Then let Certbot configure Nginx and redirect HTTP to HTTPS:
sudo certbot --nginx --redirect \
-d example.org -d www.example.org Test the certificate and the automatic renewal path:
curl -I https://example.org
sudo certbot renew --dry-run
systemctl list-timers --all | grep certbot Do not enable HSTS until HTTPS works for every hostname and all required subdomains are ready to remain HTTPS-only.
Configure WordPress
Copy the sample configuration file and edit the database settings:
sudo cp /var/www/wordpress/wp-config-sample.php \
/var/www/wordpress/wp-config.php
sudo nano /var/www/wordpress/wp-config.php Set the database name, restricted account, generated password, and local database host:
define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'wp_site' );
define( 'DB_PASSWORD', 'REPLACE_WITH_THE_GENERATED_PASSWORD' );
define( 'DB_HOST', 'localhost' ); Replace the sample authentication keys and salts with fresh values. Generate them from the official WordPress API over HTTPS and paste the complete output into wp-config.php:
curl -fsS https://api.wordpress.org/secret-key/1.1/salt/ Keep debugging disabled on the public site and disable the dashboard's built-in PHP editor:
define( 'WP_DEBUG', false );
define( 'DISALLOW_FILE_EDIT', true ); Protect the configuration file and set PHP-FPM upload limits:
sudo chown root:www-data /var/www/wordpress/wp-config.php
sudo chmod 0640 /var/www/wordpress/wp-config.php
sudo tee /etc/php/8.5/fpm/conf.d/99-wordpress.ini >/dev/null <<'EOF'
upload_max_filesize = 32M
post_max_size = 34M
memory_limit = 256M
max_execution_time = 120
EOF
sudo systemctl restart php8.5-fpm Complete the Web Installer
Open https://example.org in a browser and complete the WordPress setup screen:
- Choose the site language and enter a site title.
- Create a unique administrator name; do not use admin.
- Use a unique password from a password manager and an email address you can access.
- Sign in and confirm that the dashboard is served over HTTPS.
Confirm that the home page loads, a permalink resolves, and an image can be uploaded. Under Settings → Permalinks, choose the desired permalink structure and save it; Nginx routes those requests through index.php.
Verify the Installation
Run the service, version, listener, and permission checks:
sudo systemctl is-active nginx php8.5-fpm mysql
nginx -v
php8.5 -v
mysql --version
sudo ss -ltnp | grep -E ':(80|443|3306)\b'
sudo ufw status verbose
namei -l /var/www/wordpress/wp-config.php
curl -I https://example.org Nginx should answer on ports 80 and 443, while MySQL should not be reachable through a public interface. Check recent service logs if a page returns an error:
sudo journalctl -u nginx -u php8.5-fpm -u mysql --since today
sudo tail -n 100 /var/log/nginx/error.log Updates and Backups
Apply Ubuntu, PHP, Nginx, and MySQL security updates regularly:
sudo apt update sudo apt full-upgrade
Update plugins and themes from the WordPress dashboard, reviewing each change before applying it. The core files are root-owned by design, so deploy a WordPress core update from SSH rather than making the entire document tree writable by PHP. Back up the database and files first, download the current stable archive, and replace only the core files:
cd /tmp
curl -fSLO https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz
sudo rsync -a --delete \
--exclude=/wp-config.php \
--exclude=/wp-content/ \
/tmp/wordpress/ /var/www/wordpress/
sudo chown -R root:www-data /var/www/wordpress
sudo chown -R wordpress:www-data /var/www/wordpress/wp-content
sudo find /var/www/wordpress -type d -exec chmod 0750 {} +
sudo find /var/www/wordpress -type f -exec chmod 0640 {} +
sudo find /var/www/wordpress/wp-content -type d -exec chmod 2750 {} +
sudo find /var/www/wordpress/wp-content -type f -exec chmod 0640 {} + After replacing the files, sign in to the dashboard and complete any requested database upgrade. Keep wp-config.php and wp-content outside the replacement operation. Verify that /tmp/wordpress contains the extracted release before running rsync --delete.
For a consistent backup, save both the MySQL database and the WordPress files to protected storage:
The database command below assumes local socket authentication for MySQL root. If the installer configured a root password instead, add -u root -p to mysqldump and enter that password when prompted.
sudo install -d -o root -g root -m 0700 /var/backups/wordpress
sudo mysqldump --single-transaction wordpress \
| gzip \
| sudo tee "/var/backups/wordpress/database-$(date +%F).sql.gz" >/dev/null
sudo tar -C /var/www -czf \
"/var/backups/wordpress/files-$(date +%F).tar.gz" wordpress
sudo chmod 0600 /var/backups/wordpress/* Store an additional encrypted copy off the server and test restoration on a separate host. A backup that has never been restored is not a verified recovery plan.
Operational Checklist
- Keep the operating system, PHP, WordPress core, themes, and plugins supported and patched.
- Monitor disk space, PHP-FPM worker saturation, Nginx errors, failed logins, and certificate expiry.
- Review plugin provenance and remove components that are no longer maintained or needed.
- Keep MySQL private; grant the WordPress account access only to its own database.
- Retain database and file backups off-host and test a restore after major changes.
A WordPress installation is ready for public use when HTTPS, DNS, database isolation, file permissions, updates, and recovery have each been verified. The installer finishing successfully is only the first check.