In order to follow this guide, you should have a server with Ubuntu 24.04 (LTS) installed with a minimum of 2GB RAM and 20GB hard drive space, and access to sudo privileges. You will also need a domain name pointed to your server’s IP address.
Before we install any software, it’s always a good idea to update repository and software packages. SSH into your Ubuntu 24.04 (LTS) server and enter the below commands.
sudo apt update
sudo apt upgrade -y
Before installing services, let’s configure the firewall to allow only necessary ports. Ubuntu comes with UFW (Uncomplicated Firewall) pre-installed.
# Allow SSH (important - do this first!) sudo ufw allow 22/tcp # Allow HTTP and HTTPS sudo ufw allow 80/tcp sudo ufw allow 443/tcp # Enable the firewall sudo ufw enable # Check firewall status sudo ufw status
NGINX is a high performance web server. It also can be used as a reverse proxy. Enter this command to install NGINX Web server.
sudo apt install nginx -y
After it’s installed, we can enable Nginx to auto-start when Ubuntu is booted by running the following command.
sudo systemctl enable nginx
Then start Nginx with this command:
sudo systemctl start nginx
Now check out its status.
systemctl status nginx
MariaDB is a drop-in replacement for MySQL. It is developed by former members of MySQL team who concerned that Oracle might turn MySQL into a closed-source product. Many Linux distributions (Arch Linux, Fedora etc), companies and organisations (Google, Wikipedia) have migrated to MariaDB.
sudo apt install mariadb-server mariadb-client -y
To enable MariaDB to automatically start when Ubuntu is rebooted:
sudo systemctl enable mariadb
Start MariaDB service:
sudo systemctl start mariadb
Check its status:
systemctl status mariadb
Now run the post-installation security script:
sudo mysql_secure_installation
When it asks you to enter MariaDB root password, press Enter because you have not set the root password yet. Then enter Y to set the root password for MariaDB server. Next, press Enter to answer all the remaining questions with the default (Yes). This will remove anonymous user, disable remote root login and remove test database. This step is a basic requirement for MariaDB database security.
Create a custom MariaDB configuration file for WordPress optimization:
sudo nano /etc/mysql/mariadb.conf.d/99-wordpress.cnf
Add the following configuration:
[mysqld] # WordPress optimizations max_allowed_packet = 64M innodb_buffer_pool_size = 256M innodb_log_file_size = 64M innodb_flush_log_at_trx_commit = 2 innodb_flush_method = O_DIRECT # Connection settings max_connections = 200 wait_timeout = 60 interactive_timeout = 60 # Query cache (if needed for older WordPress versions) query_cache_type = 1 query_cache_size = 32M query_cache_limit = 2M
Restart MariaDB to apply changes:
sudo systemctl restart mariadb
Change database name and user and remember to change the password to a secure one. Use a strong password generator for production sites.
mysql -u root -p
CREATE DATABASE nunosarmento DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; GRANT ALL ON nunosarmento.* TO 'nunosarmentouser'@'localhost' IDENTIFIED BY 'YourSecurePassword123!'; FLUSH PRIVILEGES; EXIT;
Note: Using utf8mb4 instead of utf8 ensures full Unicode support, including emoji characters.
Enter the following commands to install repository, PHP 8.3 and PHP 8.3 extensions.
PHP 8.3 Installation
sudo apt install software-properties-common -y sudo add-apt-repository ppa:ondrej/php -y sudo apt update sudo apt install php8.3 php8.3-fpm php8.3-mysql php8.3-common php8.3-gd php8.3-cli php8.3-curl php8.3-mbstring php8.3-xml php8.3-xmlrpc php8.3-zip php8.3-intl php8.3-imagick php8.3-bcmath php8.3-soap -y
After the installation has completed, confirm that PHP 8.3 has installed correctly with this command:
php-fpm8.3 -v
Configure the user and group that the service will run under:
sudo nano /etc/php/8.3/fpm/pool.d/www.conf
Verify these settings (they should already be correct, but confirm):
user = www-data group = www-data listen.owner = www-data listen.group = www-data
Configure PHP for WordPress by changing some values in php.ini:
sudo nano /etc/php/8.3/fpm/php.ini
Search for and modify these values:
file_uploads = On allow_url_fopen = On upload_max_filesize = 100M post_max_size = 100M memory_limit = 256M max_execution_time = 360 max_input_vars = 5000 max_input_time = 1000 display_errors = Off log_errors = On error_log = /var/log/php8.3-fpm-errors.log expose_php = Off
Security Enhancement: Disable dangerous PHP functions:
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,show_source
Reload php8.3-fpm:
sudo systemctl reload php8.3-fpm
Check status:
systemctl status php8.3-fpm
To test the CLI version of PHP:
php --version
WP-CLI is the command-line interface for WordPress. You can update plugins, configure multisite installations and much more, without using a web browser.
cd /tmp curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar chmod +x wp-cli.phar sudo mv wp-cli.phar /usr/local/bin/wp wp --info
Create NGINX configuration file (change the domain name to your domain name):
sudo nano /etc/nginx/sites-available/nuno-sarmento.com
Paste the following text into the file. Replace “server_name” with your actual domain name and the website “root” location:
server {
listen 80;
listen [::]:80;
server_name nuno-sarmento.com www.nuno-sarmento.com;
root /var/www/nuno-sarmento.com;
index index.php index.html index.htm;
# Log files
access_log /var/log/nginx/nuno-sarmento.com.access.log;
error_log /var/log/nginx/nuno-sarmento.com.error.log;
# Max upload size
client_max_body_size 100M;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
Save and close the file. Then create symlink to nginx sites-enabled directory:
sudo ln -s /etc/nginx/sites-available/nuno-sarmento.com /etc/nginx/sites-enabled/
Create your virtual host website directory:
sudo mkdir -p /var/www/nuno-sarmento.com
Set permissions to the virtual host directory:
sudo chown -R www-data:www-data /var/www/nuno-sarmento.com
Check if your NGINX configuration is valid:
sudo nginx -t
If the test is successful, reload NGINX:
sudo systemctl reload nginx
cd /tmp
curl -LO https://wordpress.org/latest.tar.gz
tar xzvf latest.tar.gz
cp /tmp/wordpress/wp-config-sample.php /tmp/wordpress/wp-config.php
sudo cp -a /tmp/wordpress/. /var/www/nuno-sarmento.com/
sudo chown -R www-data:www-data /var/www/nuno-sarmento.com
cd /var/www/nuno-sarmento.com
sudo find . -type d -exec chmod 755 {} \;
sudo find . -type f -exec chmod 644 {} \;
Edit the WordPress wp-config.php with the details from Step 6:
sudo nano /var/www/nuno-sarmento.com/wp-config.php
Update the database credentials:
define( 'DB_NAME', 'nunosarmento' ); define( 'DB_USER', 'nunosarmentouser' ); define( 'DB_PASSWORD', 'YourSecurePassword123!' ); define( 'DB_HOST', 'localhost' ); define( 'DB_CHARSET', 'utf8mb4' ); define( 'DB_COLLATE', '' );
Generate and add WordPress Salt Keys:
Visit https://api.wordpress.org/secret-key/1.1/salt/ and copy the generated keys, then replace the dummy values in wp-config.php.
Add additional security configurations to wp-config.php:
# Add these lines before "That's all, stop editing!" # Security enhancements define( 'DISALLOW_FILE_EDIT', true ); define( 'DISALLOW_FILE_MODS', false ); define( 'WP_POST_REVISIONS', 5 ); define( 'AUTOSAVE_INTERVAL', 300 ); define( 'WP_AUTO_UPDATE_CORE', 'minor' ); # Force SSL for admin define( 'FORCE_SSL_ADMIN', true ); # Increase memory limit define( 'WP_MEMORY_LIMIT', '256M' ); define( 'WP_MAX_MEMORY_LIMIT', '512M' );
Set proper permissions for wp-config.php:
sudo chmod 640 /var/www/nuno-sarmento.com/wp-config.php sudo chown www-data:www-data /var/www/nuno-sarmento.com/wp-config.php
In order to secure/harden your WordPress you will need to add some nginx rules. Create a common directory for reusable Nginx configuration:
sudo mkdir -p /etc/nginx/common sudo nano /etc/nginx/common/wordpress.conf
Add comprehensive WordPress-specific Nginx rules:
# WordPress security and optimization rules
# Deny access to sensitive files
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
}
location ~* wp-config.php {
deny all;
}
location ~* /\.ht {
deny all;
}
location ~* /readme\.html {
deny all;
}
location ~* /readme\.txt {
deny all;
}
location ~* /license\.txt {
deny all;
}
location ~* /xmlrpc\.php {
deny all;
}
# Deny access to WordPress installation files
location = /wp-admin/install.php {
deny all;
}
location = /wp-admin/upgrade.php {
deny all;
}
# Cache static files
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires max;
log_not_found off;
access_log off;
add_header Cache-Control "public, immutable";
}
# Deny access to backup and log files
location ~* \.(bak|config|sql|fla|psd|ini|log|sh|inc|swp|dist)$ {
deny all;
}
# Prevent access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# WordPress: deny wp-content, wp-includes php files
location ~* ^/(?:wp-content|wp-includes)/.*\.php$ {
deny all;
}
# WordPress: deny general stuff
location ~* ^/(?:xmlrpc\.php|wp-links-opml\.php|wp-config\.php|wp-config-sample\.php|wp-comments-post\.php|readme\.html|license\.txt)$ {
deny all;
}
# Rate limiting for wp-login.php
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=2r/m;
location = /wp-login.php {
limit_req zone=wplogin burst=2 nodelay;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
}
# Rate limiting for wp-cron.php
location = /wp-cron.php {
limit_req zone=wplogin;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
}
Update your virtual host configuration to include these rules:
sudo nano /etc/nginx/sites-available/nuno-sarmento.com
Add the include directive after the log files section:
server {
listen 80;
listen [::]:80;
server_name nuno-sarmento.com www.nuno-sarmento.com;
root /var/www/nuno-sarmento.com;
index index.php index.html index.htm;
# Log files
access_log /var/log/nginx/nuno-sarmento.com.access.log;
error_log /var/log/nginx/nuno-sarmento.com.error.log;
# WordPress security and optimization rules
include common/wordpress.conf;
# Max upload size
client_max_body_size 100M;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
Test and reload Nginx:
sudo nginx -t sudo systemctl reload nginx
“HTTP security headers are a fundamental part of website security. Upon implementation, they protect you against the types of attacks that your site is most likely to come across. These headers protect against XSS, code injection, clickjacking, etc.”
Edit the main Nginx configuration:
sudo nano /etc/nginx/nginx.conf
Add these headers inside the http {} block:
# Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval';" always; add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; # Hide Nginx version server_tokens off; # Additional security more_clear_headers Server;
For the more_clear_headers directive to work, install the headers-more module:
sudo apt install libnginx-mod-http-headers-more-filter -y
Reload Nginx:
sudo systemctl reload nginx
You can check your websites headers security on the link below:
https://securityheaders.com/
On Ubuntu 24.04, Certbot is available directly from the Ubuntu repositories:
sudo apt install certbot python3-certbot-nginx -y
Obtain and install SSL certificate (replace with your domain):
sudo certbot --nginx -d nuno-sarmento.com -d www.nuno-sarmento.com
LetsEncrypt steps:
1 – Enter email address (used for urgent renewal and security notices): hello@nuno-sarmento.com
2 – Please read the Terms of Service. You must agree in order to register with the ACME server.
(A)gree/(C)ancel: A
3 – Would you be willing to share your email address with the Electronic Frontier Foundation?
(Y)es/(N)o: Y
4 – Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
1: No redirect
2: Redirect – Make all requests redirect to secure HTTPS access.
Select the appropriate number [1-2] then [enter]: 2
Test automatic renewal:
sudo certbot renew --dry-run
To manually renew SSL certificates:
sudo certbot renew
Test your SSL configuration:
https://www.ssllabs.com/ssltest/
Fail2Ban protects your server from brute force attacks by banning IPs that show malicious signs.
sudo apt install fail2ban -y
Create a local configuration file:
sudo nano /etc/fail2ban/jail.local
Add the following configuration:
[DEFAULT] bantime = 3600 findtime = 600 maxretry = 5 destemail = hello@nuno-sarmento.com sendername = Fail2Ban action = %(action_)s bantime.increment = true bantime.factor = 24 bantime.maxtime = 4w [sshd] enabled = true port = 22 filter = sshd logpath = /var/log/auth.log maxretry = 3 action = %(action_)s [nginx-http-auth] enabled = true port = http,https filter = nginx-http-auth logpath = /var/log/nginx/*error.log [nginx-noscript] enabled = false port = http,https filter = nginx-noscript logpath = /var/log/nginx/*access.log maxretry = 6 [nginx-badbots] enabled = false port = http,https filter = nginx-badbots logpath = /var/log/nginx/*access.log maxretry = 2 [nginx-noproxy] enabled = false port = http,https filter = nginx-noproxy logpath = /var/log/nginx/*access.log maxretry = 2 [nginx-limit-req] enabled = true port = http,https filter = nginx-limit-req logpath = /var/log/nginx/*error.log [wordpress-hard] enabled = true port = http,https filter = wordpress-hard logpath = /var/log/nginx/*access.log maxretry = 3
Create WordPress-specific filter:
sudo nano /etc/fail2ban/filter.d/wordpress-hard.conf
[Definition]
failregex = ^<HOST> .* "POST /wp-login.php
^<HOST> .* "POST /xmlrpc.php
ignoreregex =
Enable and start Fail2Ban:
sudo systemctl enable fail2ban sudo systemctl start fail2ban
Check Fail2Ban status:
sudo fail2ban-client status sudo fail2ban-client restart sudo fail2ban-client status wordpress-hard
Replace WordPress built-in cron (wp-cron.php) with system cron for better performance.
Disable WordPress cron in wp-config.php:
sudo nano /var/www/nuno-sarmento.com/wp-config.php
Add before “That’s all, stop editing!”:
define( 'DISABLE_WP_CRON', true );
Create a system cron job:
sudo crontab -e -u www-data
Add this line (runs every 5 minutes):
*/5 * * * * /usr/local/bin/wp cron event run --due-now --path=/var/www/nuno-sarmento.com > /dev/null 2>&1
Redis dramatically improves WordPress performance by caching database queries.
sudo apt install redis-server php8.3-redis -y
Enable and start Redis:
sudo systemctl enable redis-server sudo systemctl start redis-server
Configure Redis for security:
sudo nano /etc/redis/redis.conf
Find and modify these settings:
bind 127.0.0.1 ::1 maxmemory 256mb maxmemory-policy allkeys-lru
Restart Redis and PHP-FPM:
sudo systemctl restart redis-server sudo systemctl restart php8.3-fpm
Install Redis Object Cache plugin via WP-CLI:
sudo -u www-data wp plugin install redis-cache --activate --path=/var/www/nuno-sarmento.com sudo -u www-data wp redis enable --path=/var/www/nuno-sarmento.com
Create automated database backups using a bash script.
sudo mkdir -p /var/backups/wordpress sudo nano /usr/local/bin/wordpress-backup.sh
#!/bin/bash DB_NAME="nunosarmento" DB_USER="nunosarmentouser" DB_PASS="YourSecurePassword123!" BACKUP_DIR="/var/backups/wordpress" SITE_DIR="/var/www/nuno-sarmento.com" DATE=$(date +%Y%m%d_%H%M%S) mkdir -p $BACKUP_DIR mysqldump -u $DB_USER -p$DB_PASS $DB_NAME | gzip > $BACKUP_DIR/db_backup_$DATE.sql.gz tar -czf $BACKUP_DIR/files_backup_$DATE.tar.gz $SITE_DIR find $BACKUP_DIR -type f -name "*.gz" -mtime +7 -delete echo "Backup completed: $DATE"
Make the script executable:
sudo chmod +x /usr/local/bin/wordpress-backup.sh
Create a cron job for daily backups at 2 AM:
sudo crontab -e
0 2 * * * /usr/local/bin/wordpress-backup.sh >> /var/log/wordpress-backup.log 2>&1
Error Nginx: 413 – Request Entity Too Large Error and Solution
The error “413 – Request Entity Too Large” indicates that web server configured to restrict large file size. To fix this edit your nginx.conf:
sudo nano /etc/nginx/nginx.conf
Add inside the http {} block:
# Set client body size to 100M client_max_body_size 100M;
Reload Nginx:
sudo systemctl reload nginx
502 Bad Gateway Error
Usually indicates PHP-FPM is not running:
sudo systemctl status php8.3-fpm sudo systemctl restart php8.3-fpm sudo tail -f /var/log/nginx/error.log
Database Connection Error
Verify database credentials in wp-config.php:
mysql -u nunosarmentouser -p nunosarmento
Service Management:
# Restart services sudo systemctl restart nginx sudo systemctl restart php8.3-fpm sudo systemctl restart mariadb sudo systemctl restart redis-server # Check service status sudo systemctl status nginx sudo systemctl status php8.3-fpm sudo systemctl status mariadb # View logs sudo tail -f /var/log/nginx/error.log sudo tail -f /var/log/nginx/nuno-sarmento.com.error.log
WP-CLI Common Commands:
# Update WordPress core sudo -u www-data wp core update --path=/var/www/nuno-sarmento.com # Update all plugins sudo -u www-data wp plugin update --all --path=/var/www/nuno-sarmento.com # List plugins sudo -u www-data wp plugin list --path=/var/www/nuno-sarmento.com # Database optimization sudo -u www-data wp db optimize --path=/var/www/nuno-sarmento.com # Search and replace (useful for domain changes) sudo -u www-data wp search-replace 'old-domain.com' 'new-domain.com' --path=/var/www/nuno-sarmento.com
✅ UFW firewall enabled with only necessary ports open
✅ Fail2Ban installed and configured
✅ SSL certificate installed with A+ rating
✅ Security headers configured
✅ WordPress security constants set in wp-config.php
✅ File permissions hardened
✅ SSH root login disabled (recommended)
✅ Database user has minimal required privileges
✅ PHP dangerous functions disabled
✅ WordPress auto-updates enabled
✅ Regular backups scheduled
✅ System cron replacing wp-cron.php
✅ Redis cache installed (optional)
✅ Security plugins installed (recommended)
✅ XML-RPC disabled
Congratulations! You now have a secure, optimized, and production-ready WordPress installation on Ubuntu 24.04 with Nginx, PHP 8.3, MariaDB, and comprehensive security hardening.