Table of Contents

Most people install WordPress the easy way. We’re going to install it the right way. secure, repeatable and for what it’s worth, it’s still easy.

WordPress doesn’t get hacked because it’s bad. It gets hacked because people cut corners.

WordPress powers more than a third of the web and while it is famous for its five-minute install the truth is that most people set it up in a way that leaves cracks open for attackers and headaches down the road. Installing it the right way is not about making things harder, it is about laying a solid foundation for performance, security, and stability. A rushed or copy-pasted configuration often hides dangerous flaws that bots and scanners know how to exploit. When you take a few extra minutes to configure the database properly, lock down PHP, and use a hardened Nginx setup you are protecting yourself from problems that can take entire sites offline. Think of it like building a house: anyone can put up four walls, but only a careful build will survive a storm. This guide walks through the complete process step by step so you get the benefits of WordPress without the common mistakes.
Alright, Let’s skip the chit-chat

What we’ll be using?

  • System: Ubuntu 22.04 or 24.04 is recommended
  • Database: MariaDB for performance and stability
  • Webserver: NGINX for performance and stability
  • Caching: Redis- high-performance, in-memory data store
  • SSL: CertBot
  • Extras: Nginx Auto-Reload, Brotli Compression, Secure Logging, Email Obfuscation

Ubuntu has become my go-to server operating system ever since Red Hat “Killed” CentOS. The ease of use, extensive documentation, strong community support, and wide availability of up-to-date software packages make it a natural fit for both beginners and professionals. It strikes the right balance between stability and modern features, which is exactly what you want when you’re running something as widely targeted as WordPress.

Prepare Your Server

Bash
sudo apt install python-software-properties
sudo add-apt-repository ppa:ondrej/nginx
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx nginx-extras mariadb-server unzip curl redis-server imagemagick
sudo apt install -y php8.3-fpm php8.3-mysql \
 php8.3-curl php8.3-xml php8.3-zip php8.3-gd php8.3-mbstring php8.3-bcmath \
 php8.3-intl php8.3-soap php8.3-imagick
Bash
# Allow NGINX in the Firewall (For this instance UFW)
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

As always, I do recommend disabling ssh root login via ssh_config and disabling password authentication and resorting to SSH Keys for safety reasons

Setup the Database

Run the built-in hardener

Bash
sudo mysql_secure_installation
Switch to unix_socket authentication [Y/n] N
Change the root password? [Y/n] As you wish
Remove anonymous users? [Y/n] Y
Disallow root login remotely? [Y/n] Y
Remove test database and access to it? [Y/n] Y
Reload privilege tables now? [Y/n] Y

Create a dedicated database + user

SQL
# Change the DB/User Names, Password as you wish
CREATE DATABASE wp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'VERY_STRONGPASS!';
GRANT ALL PRIVILEGES ON wp.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;

Recommended DB Tuning, Edit /etc/mysql/mariadb.conf.d/50-server.cnf

INI
[mysqld]
bind-address            = 127.0.0.1
# Adjust as you like.
# ~50-70% of RAM on DB-only servers
innodb_buffer_pool_size = 1G 
innodb_log_file_size    = 256M
innodb_flush_method     = O_DIRECT
max_connections         = 200
query_cache_size        = 0
query_cache_type        = 0
Bash
sudo systemctl restart mariadb

Optimize PHP for WordPress

Open /etc/php/8.3/fpm/php.ini or 8.4 and uncomment/append the following

INI
memory_limit = 256M
; These values are kinda low. So adjust as you like
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 120

; Security: prevent path-info exploits
cgi.fix_pathinfo = 0
expose_php = Off

; Enable OPcache
zend_extension=opcache
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000
opcache.validate_timestamps=1
Bash
# Reload PHP-fpm
sudo systemctl reload php8.3-fpm # or 8.4

Download WordPress via WP-CLI

Install WP-CLI

Bash
sudo -u ubuntu -i # Login as a user other than the root
sudo apt install -y php-cli php-mbstring unzip less file
curl -sS -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x /usr/local/bin/wp

Create a web root

Bash
mkdir -p /var/www/blog
cd /var/www/blog

Download & Configure

Bash
wp core download --locale=en_US
# The DB/User/Password you created earlier
wp config create --dbname=wp --dbuser=wpuser --dbpass=VERY_STRONGPASS! --dbhost=localhost --dbprefix=wp_
# Do not use default username: admin, Choose something more secure
wp core install --url="https://example.com" --title="My Site" --admin_user="myusername" --admin_password="ANOTHERSTRONGPASS" --admin_email="me@example.com"
sudo chown -R www-data:www-data /var/www/blog
find /var/www/blog -type d -exec chmod 755 {} \;
find /var/www/blog -type f -exec chmod 644 {} \;
# WP-CONFIG should always have 600 or 440 (-rw-------) permission
chmod 600 /var/www/blog/wp-config.php
# https://developer.wordpress.org/advanced-administration/security/hardening/#file-permissions

Secure and Optimize WordPress

Bash
# Set unique salts & keys
wp config shuffle-salts

Editwp-config.php and append the following

PHP
# Disable file editing inside WP-Admin
define('DISALLOW_FILE_EDIT', true);
# Limit Revisions to 5
define('WP_POST_REVISIONS', 5);
# Disable WP cron (use system cron)
define('DISABLE_WP_CRON', true);
# Save/Exit the file
Bash
sudo -u ubuntu crontab -e
# Append the following at the end of the file
* * * * * /usr/local/bin/wp --path=/var/www/blog cron event run --due-now --quiet

Setup NGINX Web Server

First, Let’s setup Certbot to Issue an SSL Certificate | Cloudflare API KEY, If you are not using Cloudflare, Find your provider here or use HTTP method.
Keep in mind that “blog.conf” can be renamed anything you like. In case that wasn’t clear.

Bash
sudo apt install -y certbot python3-certbot-dns-cloudflare
sudo mkdir -p /etc/letsencrypt
echo "dns_cloudflare_api_token = YOUR_API_TOKEN_HERE" > /etc/.cloudflare.ini
sudo chmod 600 /root/.cloudflare.ini
Bash
# Issue a Certificate
sudo certbot certonly -d example.com -d "*.example.com" \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/.cloudflare.ini \
  --key-type rsa \
  --rsa-key-size 2048 \
  --agree-tos \
  --force-renewal\
  --no-eff-email\
  --email YOUR@EMAIL.COM
# The cert output should be in /etc/letsencrypt/live/example.com/

Now, Let’s setup NGINX. Config Files: In My Github. Don’t forget to change “example.com” inside the files to your domain and SSL Cert path.

Bash
nano /etc/nginx/nginx.conf
# Apend/Modify the following inside http{} block
http2 on;
ssl_protocols TLSv1.2 TLSv1.3; # Drop v1, v1.1 - Keep only 1.2 & 1.3
Bash
cd /etc/nginx/sites-available/
wget -O /etc/nginx/sites-available/blog.conf https://raw.githubusercontent.com/abdessalllam/cloud-setup/refs/heads/main/wordpress/blog.conf
# Disable directory browsing
echo "autoindex off;" > /etc/nginx/snippets/no-autoindex.conf
# Enable the Website and Reload NGINX
sudo ln -s /etc/nginx/sites-available/blog.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Enable Cloudflare Support

If you’ll be using Cloudflare for with Reverse Proxy, we need to add CF Ranges to make sure our Server gets the real IP of the Visitor. Latest Cloudflare’s IPv4IPv6 Ranges
The same can apply to other Load balancers, All you have to do is modify the file below to include their IP Ranges “only”.

Bash
wget -O /etc/nginx/conf.d/realip.conf https://raw.githubusercontent.com/abdessalllam/cloud-setup/refs/heads/main/wordpress/realip.conf
sudo nginx -t && sudo systemctl reload nginx

All .conf files under conf.d will be included automatically by default
Next, Create ip.php in /var/www/blog folder and test if Nginx is getting the real IP.

PHP
<?php
header('Content-Type: text/plain');
foreach (['REMOTE_ADDR','HTTP_CF_CONNECTING_IP','HTTP_X_FORWARDED_FOR'] as $h) {
    echo "$h: " . ($_SERVER[$h] ?? '') . "\n";
}

Now, Install Cloudflare plugin and apply recommended settings.

Setup Redis Caching

Redis is one of the best ways to speed up WordPress (object caching, sometimes page caching)

Bash
sudo systemctl enable redis-server
sudo systemctl start redis-server

Secure Redis by editing /etc/redis/redis.conf

Config
# Append/Uncomment/Modify the following
supervised systemd
maxmemory 256mb                # adjust for your RAM
maxmemory-policy allkeys-lru   # evict least-used keys

Install Essential Plugins

Bash
wp plugin install redis-cache --activate
wp redis enable
wp plugin install nginx-helper --activate

Install & Setup Brotli

Brotli makes your website load faster by sending fewer bytes over the internet — like zipping files before emailing them, but automatic and invisible to the user. It’s better than Gzip.

Bash
# Only with ppa:ondrej/nginx Repo
sudo apt install libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static
Bash
nano /etc/nginx/nginx.conf
Config
# Append the following to inside http{} block, above "gzip on"
brotli on;
brotli_comp_level 5;
brotli_static on;  # serve precompressed .br if present (harmless if not)
brotli_types
  text/plain text/css text/javascript application/javascript
  application/json application/xml application/rss+xml
  application/vnd.ms-fontobject application/font-sfnt
  application/x-font-ttf font/ttf font/otf image/svg+xml;
  
# Append/Uncomment the following
gzip on;
gzip_comp_level 6;
gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml;

Reload NGINX & Test if Brotli is working

Bash
sudo nginx -t && sudo systemctl reload nginx
# Brotli client
curl -sI -H 'Accept-Encoding: br' https://yourdomain | grep -i content-encoding
# Response → Content-Encoding: br

# Gzip client
curl -sI -H 'Accept-Encoding: gzip' https://yourdomain | grep -i content-encoding
# Response → Content-Encoding: gzip (or nothing if already tiny)

NGINX Auto-Reload

if you start using plugins such as Smush, Yoast SEO Redirects, etc… Any plugin that requires include files in “blog.conf” or “nginx.conf”. Everytime the plugin changes that file, we need to reload NGINX. So let’s create a service that does that automatically.

Bash
# Create a /etc/systemd/system/nginx-auto-reload.service
[Unit]
Description=Validate and reload Nginx when watched files change

[Service]
Type=oneshot
# Validate config & Stop if there are errors
ExecStart=/usr/sbin/nginx -t
# Reload ONLY if ExecStart succeeded
ExecStartPost=/bin/systemctl reload nginx
Bash
# Create /etc/systemd/system/nginx-auto-reload.path
[Unit]
Description=Watch specific Nginx-related files

[Path]
# Add one line per file you want to monitor for changes
# Add/Remove Lines as needed
PathChanged=/var/www/blog/foo.conf
PathChanged=/var/www/blog/bar.conf
PathChanged=/var/www/blog/baz.conf

[Install]
WantedBy=multi-user.target

Enable and Start

Bash
sudo systemctl daemon-reload
sudo systemctl enable --now nginx-auto-reload.path
# (optional first run)
sudo systemctl start nginx-auto-reload.service

Test: Edit one of the monitored files, then

Bash
journalctl -u nginx-auto-reload.service -n 20 --no-pager
# You should see nginx: configuration file /etc/nginx/nginx.conf test is successful followed by a reload. If the config is broken, nginx -t will fail and no reload happens.

Safe Logging

Logs are your black box recorder: they tell you what happened, when, and why. They’re essential for debugging and for spotting performance or security issues. But without rotation, logs grow endlessly and can fill your disk. Do it right, enable only what you need, store it safely, and rotate/compress on a schedule.

Bash
mkdir -p /var/log/blog
sudo chown www-data:www-data /var/log/blog
sudo chmod 750 /var/log/blog
sudo tee /etc/logrotate.d/wordpress >/dev/null <<'EOF'
/var/log/blog/debug.log {
weekly
rotate 12
compress
missingok
notifempty
create 640 www-data www-data
}
EOF
PHP
# Enable file logging only in wp-config.php
if ( ! defined( 'WP_DEBUG' ) ) {
        define( 'WP_DEBUG', true );
}
// Set display to false, We don't want errors displayed on our frontend
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );
// Log to a file (outside web root is best)
define('WP_DEBUG_LOG', '/var/log/blog/debug.log');
// Set error logging to a seperate file (optional)
@ini_set('log_errors', 1);
@ini_set('error_log', '/var/log/blog/error.log');

Extras

If you’re not using Cloudflare’s “Email Obfuscation”, I recommend this plugin to protect your email address from bots and scrapers. It ensures your email is only shown when JavaScript is enabled, keeping it hidden from the page source and harder for spam bots to harvest.

Bash
# Let's create a Must-Use folder
mkdir -p /var/www/blog/wp-content/mu-plugins 
chown ubuntu:www-data //var/www/blog/wp-content/mu-plugins
wget -O /var/www/blog/wp-content/mu-plugins/protect-my-email.php https://raw.githubusercontent.com/abdessalllam/cloud-setup/refs/heads/main/wordpress/protect-my-email.php

Usage:

Editor
Full Shortcode: [js_email label="Reach out to me via Email:" user="info" domain="example.com" display="inline" subject="Hello" msg="Please enable JS for the Email" class="optional-extra-class"]
Minimal Use: [js_email user="info" domain="example.com"]
Output: info@example.com

What’s next?

Nginx logs are located at /var/log/nginx/

That’s it, You are done. Congratulations 🎉🎉 I’d recommend Installing a Cache Plugin and Security Plugin to help speedup and secure your Setup further.

This isn’t the quickest way to install WordPress — but it’s the right way.
Have suggestions or spotted something we could improve? Share your thoughts in the comments — we’d love to hear from you.

 

 

Categorized in:

Guides,