In this tutorial, we’ll walk through the process of setting up a WordPress site on Ubuntu 22.04 LTS, leveraging the power and performance of MySQL 8.1, Nginx, and PHP. We’ll explore the installation and configuration of these core components, focusing on two major PHP versions: 8.1 for those who want the latest features and improvements, and 7.4 for users requiring compatibility with older plugins or themes. This guide aims to provide a clear, step-by-step approach to building a WordPress environment that is optimized for speed and reliability, ensuring your site runs smoothly on the latest web technologies.

I’ve dedicated several hours to crafting this content. If you find these kinds of posts valuable and wish to support my efforts, please consider supporting me by using my, please consider using my Digital Ocean referral code.

Prepping the server in case you want to SFTP or SSH in at a later time as non-root.

sudo adduser newusername && sudo usermod -aG sudo newusername
su newusername
cd ~
sudo apt update -y && sudo apt upgrade -y
sudo apt install redis -y

Install MySQL

This segment outlines the steps to install and configure MySQL on an Ubuntu system, specifically using the Percona Server variant, which is an enhanced, fully compatible replacement for the MySQL database. The process begins with downloading the Percona repository package and installing necessary dependencies. After updating the package list, the script enables the Percona Server 8.x version and proceeds to install the Percona Server. During the installation, the user is prompted to set a root password and other security options. Post-installation, the mysql service is added to the system’s startup routine.

The configuration file (mysqld.cnf) is then edited to adjust the innodb_buffer_pool_size parameter, which is crucial for performance tuning. The MySQL service is restarted to apply these changes. Finally, the script creates a new database and user, granting full privileges to the user on the newly created database, ensuring the database system is ready for use with secure access.

## Install MySQL
curl -O https://repo.percona.com/apt/percona-release_latest.generic_all.deb
sudo apt install gnupg2 lsb-release ./percona-release_latest.generic_all.deb -y
sudo apt update -y
sudo percona-release enable-only ps-8x-innovation release
sudo apt update -y
sudo apt install percona-server-server -y
# Follow prompts to add root password and strong password.
sudo update-rc.d mysql defaults

# Change according to your needs
sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
innodb_buffer_pool_size = 256M
sudo systemctl restart mysql

mysql -uroot -p
CREATE DATABASE example_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'examples_user'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON example_db.* TO 'example_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Create Swapfile

This section details the creation and activation of a swap file on a Linux system, which provides additional virtual memory to support system operations when physical RAM is fully utilized. The process starts by allocating a 4GB file (/swapfile) to serve as swap space, then sets the file’s permissions to ensure it is only accessible by the system’s root user for security reasons. The mkswap command prepares this file to be used as swap space, and swapon activates it, adding it to the system’s virtual memory pool. To ensure the swap file is reactivated on system reboot, an entry is added to the /etc/fstab file. The final command, swapon --show, is used to verify that the swap is active and correctly set up, displaying its status and attributes. This approach to adding swap space is particularly useful for systems with limited RAM, helping to improve performance and stability under load.

## Create Swapfile
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
sudo swapon --show

Install PHP

This section outlines the steps for installing PHP on an Ubuntu system, with a focus on setting up multiple PHP versions to ensure compatibility with various WordPress requirements. The process begins with updating the system’s package list, followed by the installation of software-properties-common, a package that facilitates the management of independent software repositories. Next, the Ondřej Surý PPA (Personal Package Archive) for PHP is added, providing access to multiple PHP versions. After another update to the package list to recognize the newly added PPA, the script installs specific PHP versions: 8.2, 8.0, and 7.4, along with essential extensions like CLI (Command Line Interface), FPM (FastCGI Process Manager), MySQL, XML, and mbstring for each version. This approach allows the user to switch between PHP versions as needed, accommodating different WordPress version requirements and ensuring broad compatibility and performance optimization.

## Install PHP
sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository ppa:ondrej/php
sudo apt update -y

If you want multiple versions. WordPress might play friendly with one over another.
sudo apt install -y php8.2 php8.2-cli php8.2-fpm php8.2-mysql php8.2-xml php8.2-mbstring
sudo apt install -y php8.0 php8.0-cli php8.0-fpm php8.0-mysql php8.0-xml php8.0-mbstring
sudo apt install -y php7.4 php7.4-cli php7.4-fpm php7.4-mysql php7.4-xml php7.4-mbstring

PHP Config

This section guides you through optimizing PHP-FPM (FastCGI Process Manager) settings for a WordPress site running on PHP 7.4, tailored to the server’s memory capacity. The process begins by editing the www.conf file for the PHP-FPM pool configuration, which controls how PHP processes are managed.

The command provided uses ps to report the memory usage of PHP-FPM processes, aiding in calculating the optimal number of child processes the server can handle based on available memory. This information is crucial for configuring the pm (process manager) settings to balance performance and resource usage.

For a server setup with dynamic memory allocation (pm = dynamic), several key parameters are adjusted:

  • pm.process_idle_timeout: Time before an idle process is terminated. Set to 10 seconds to free up resources from inactive processes.
  • pm.max_children: Maximum number of child processes. Set to 5 for servers with limited memory, ensuring not to overallocate resources.
  • pm.start_servers: Number of child processes created on startup. Set to 2, providing a baseline capacity for handling incoming requests.
  • pm.min_spare_servers and pm.max_spare_servers: Controls the minimum and maximum number of idle child processes, set to 2 and 5 respectively to maintain a buffer for fluctuating demand without overconsuming resources.

For servers with more memory, such as a 2GB RAM droplet, the settings are adjusted to allow more child processes: pm.max_children increased to 30, pm.start_servers to 10, pm.min_spare_servers to 8, and pm.max_spare_servers to 15. These settings enable the server to handle more concurrent requests, improving the site’s responsiveness under heavier loads while still preventing memory exhaustion.

sudo vim /etc/php/7.4/fpm/pool.d/www.conf
Once you have WordPress running, run this command to find out how much memory each PHP process uses and divide the amount of free memory by that number.
ps --no-headers -o "rss,cmd" -C php-fpm7.4 | awk '{ sum+=$1 } END { printf ("%d%s\n", sum/NR/1024,"Mb") }'

pm = dynamic
pm.process_idle_timeout = 10s
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 5

You you chose a 2GB ram droplet this might work better:
pm = dynamic
pm.max_children = 30
pm.start_servers = 10
pm.min_spare_servers = 8
pm.max_spare_servers = 15


This instruction is about modifying the PHP configuration for a PHP 7.4 FPM environment to adjust file upload and memory usage limits. By using the vim text editor to open the php.ini file located in /etc/php/7.4/fpm/, you are directed to change two key settings:

  1. upload_max_filesize = 100M: This setting increases the maximum size of files that can be uploaded through PHP to 100 megabytes. It’s particularly useful for sites that need to accept large file uploads, such as media files, documents, or large images.
  2. memory_limit = 128M: This setting adjusts the maximum amount of memory that a PHP script is allowed to allocate to 128 megabytes. This limit ensures that individual scripts do not use excessive server resources, which could affect the overall performance of the server and other applications running on it.

These changes are crucial for web applications like WordPress that often require higher upload limits for media content and sufficient memory for processing complex plugins and themes. After making these adjustments, you would typically need to restart the PHP-FPM service for the changes to take effect, ensuring that your PHP environment is optimized for your specific requirements.

sudo vim /etc/php/7.4/fpm/php.ini 
upload_max_filesize = 100M
memory_limit = 128M

Nginx and PHP-FPM workers

Understanding the dynamics between Nginx workers and PHP-FPM workers is essential for optimizing server performance, akin to managing a post office and a kitchen. Nginx, likened to an efficient post office, can handle a vast number of requests with its workers acting as capable postal employees, efficiently sorting and dispatching thousands of letters. These workers excel in multitasking, ensuring swift and orderly request management.

When requests requiring PHP processing arrive, Nginx forwards them to PHP-FPM, similar to sending orders to a kitchen where PHP-FPM workers, or chefs, prepare the requested “dishes.” These workers need adequate space, or memory, to process each request, which can range from simple tasks to complex operations involving numerous plugins and themes. The memory consumption varies with the complexity of the tasks, analogous to the kitchen space used by dishes of different intricacies.

Balancing the number of PHP-FPM workers is crucial to prevent overloading the server’s memory, akin to managing kitchen staff to avoid overcrowding and ensure efficient operation during peak times. While Nginx can handle a large volume of requests, the server’s capacity to process these requests through PHP-FPM workers depends on available memory. It’s vital to adjust PHP-FPM workers based on traffic and memory constraints, ensuring the server remains stable and responsive under varying loads.

Install NGINX

In this section, we’ll cover the installation and configuration of Nginx on your server, a crucial step for setting up a high-performance web environment. Nginx is known for its efficiency and flexibility, making it an excellent choice for serving web content. We’ll start by installing Nginx and Fail2Ban, a tool to protect against unauthorized access, followed by configuring the firewall to allow web traffic. Next, we’ll delve into setting up a server block (virtual host) for your site, which involves creating and linking the necessary configuration files, and applying specific settings for handling PHP requests and caching to optimize performance and security. This comprehensive guide ensures your Nginx server is not only up and running but also finely tuned to serve your web content effectively and securely.

## Install Nginx
sudo apt update
sudo apt install nginx -y
sudo apt install fail2ban -y

Adjust the firewall if needed:
sudo ufw app list
sudo ufw allow 'Nginx HTTP'
sudo ufw allow 'Nginx HTTPS'
systemctl status nginx

Setup server block

sudo vim /etc/nginx/sites-available/example.com
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo systemctl reload nginx 
sudo nginx -t
sudo systemctl restart nginx
fastcgi_cache_path /var/run/examplecom levels=1:2 keys_zone=examplecomcache:200m max_size=128m inactive=2h use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

server {
    listen 80;
    listen [::]:80;

    server_name example.com;

    root /var/www/example.com;
    index index.php index.html index.htm index.nginx-debian.html;

    set $skip_cache 0;

    # POST requests and URLs with a query string should always skip cache
    if ($request_method = POST) {
      set $skip_cache 1;
    }

    if ($query_string != "") {
      set $skip_cache 1;
    }

    # Don't cache URLs containing the following segments
    if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
      set $skip_cache 1;
    }

    # Don't use the cache for logged-in users or recent commenters
    if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
      set $skip_cache 1;
    }

    client_max_body_size 25m;

    location / {
            try_files $uri $uri/ =404 /index.php?$args;
    }

    location ~ \.php$ {
      include snippets/fastcgi-php.conf;
      fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
      fastcgi_param HTTPS 1;

      fastcgi_cache examplecomcache;
      fastcgi_cache_valid 200 301 302 2h;
      fastcgi_cache_use_stale error timeout updating invalid_header http_500 http_503;
      fastcgi_cache_min_uses 1;
      fastcgi_cache_lock on;
      fastcgi_cache_bypass $skip_cache;
      fastcgi_no_cache $skip_cache;
      add_header X-FastCGI-Cache $upstream_cache_status;
    }

    location ~ /\.ht {
      deny all;
    }
}

Install WordPress

This section outlines the steps to install WordPress, the popular content management system, on your server. The process begins by creating a directory for your website under /var/www/example.com/. Following this, the latest WordPress package is downloaded using wget from the official WordPress site. To handle the downloaded .zip file, unzip is installed and used to extract the WordPress files. The rsync command is then employed to transfer the extracted WordPress files to the newly created website directory, ensuring that the file permissions and ownership are correctly set to www-data, which is the default web server user in Ubuntu. This ensures that the web server can correctly access and serve the WordPress files. Finally, permissions are adjusted to 755 for directories and files within the WordPress installation to ensure proper access levels, balancing security and functionality. These steps lay the foundation for a functional WordPress site, ready for configuration and customization.

sudo mkdir -p /var/www/example.com/
sudo wget https://wordpress.org/latest.zip
sudo apt-get install unzip -y
sudo unzip latest.zip
sudo rsync -av wordpress/* /var/www/example.com/
sudo chown -R www-data:www-data /var/www/example.com/
sudo chmod -R 755 /var/www/example.com

Let’s Encrypt SSL

This section guides you through securing your Nginx server with SSL/TLS certificates from Let’s Encrypt, using Certbot, an automated tool that simplifies the process. First, the system’s package list is updated to ensure you have the latest versions of the software. Then, Certbot and its Nginx plugin (python3-certbot-nginx) are installed. These tools integrate seamlessly with Nginx, making the certificate issuance process straightforward.

Following the installation, you’ll use Certbot to automatically obtain and install a Let’s Encrypt certificate for your domain (example.com), configuring Nginx in the process to serve your site over HTTPS. This step not only secures your site’s communication through encryption but also improves your site’s credibility and search engine ranking.

Let’s Encrypt certificates have a 90-day validity period, but Certbot is designed to handle renewals automatically through scheduled tasks (cron jobs or systemd timers). The --dry-run option for renewal testing simulates the process without making any actual changes, ensuring that automatic renewals are set up correctly and will function as intended when required. This setup is crucial for maintaining continuous HTTPS protection without manual intervention.

sudo apt update -y
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d example.com

# Let's Encrypt certificates are valid for 90 days. Certbot should automatically set up a cron job or systemd timer to renew the certificates. You can test the renewal process with:
sudo certbot renew --dry-run

ENABLE HTTP STRICT TRANSPORT SECURITY (HSTS)

Building on the secure foundation provided by Let’s Encrypt SSL/TLS certificates, this section delves into the implementation of HTTP Strict Transport Security (HSTS) on your Nginx server. HSTS is a security feature that instructs browsers to connect to your site using HTTPS only, further strengthening your site’s security posture. After successfully deploying SSL/TLS certificates, configuring HSTS is a crucial next step. It not only reinforces your site’s encryption but also mitigates certain types of attacks, such as man-in-the-middle (MITM) attacks. By implementing HSTS, you ensure that users benefit from continuous security enhancements, maintaining the integrity and confidentiality of data exchanged between your site and its visitors.

  1. Open your Nginx configuration file for the site, typically found in /etc/nginx/sites-available/ or directly within /etc/nginx/nginx.conf, depending on your setup.
  2. Locate your HTTPS server block, which should be listening on port 443.
  3. Add the Strict-Transport-Security header within the server block:
server {
    listen 443 ssl;
    server_name example.com;

    # Other SSL configurations like ssl_certificate, ssl_certificate_key, etc.

    # Enable HSTS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Rest of your server block configuration...
}
  • max-age specifies the number of seconds that the browser should automatically convert all HTTP requests to HTTPS. 31536000 seconds equals 1 year.
  • includeSubDomains ensures that the rule applies to all subdomains as well.

HTTP/2 SUPPORT

This section guides you through enabling HTTP/2 support in Nginx, a protocol that significantly improves the efficiency and speed of data transfer between servers and clients. To start, you’ll need to confirm that your Nginx version is compiled with the HTTP/2 module, which is common in contemporary Nginx installations. The process involves checking your Nginx version and its compiled modules for HTTP/2 support.

Once you’ve verified HTTP/2 support, the next step involves configuring Nginx to utilize HTTP/2 in your SSL server blocks. It’s important to note that HTTP/2 should only be enabled for SSL configurations since browsers require secure connections to use HTTP/2. This entails adjusting the listen directive in your Nginx configuration files to include the http2 keyword alongside ssl, ensuring that your server is ready to handle HTTP/2 requests over secure connections.

Finally, you’ll test the configuration using tools like curl to confirm that HTTP/2 is actively working on your domain. This enhancement not only boosts the performance of your website but also improves user experience by making web pages load faster and more efficiently, leveraging the advanced features of HTTP/2.

To enable HTTP/2 support in Nginx, you need to ensure that Nginx is compiled with HTTP/2 module support and then configure it to use HTTP/2 in your server blocks. Most modern Nginx installations come with HTTP/2 support out of the box. Here’s how to enable it:

Step 1: Check Nginx HTTP/2 Support

First, verify that your Nginx version supports HTTP/2. You can do this by checking the Nginx version and compiled modules:

nginx -V

Look for the --with-http_v2_module in the output. If it’s present, your Nginx build supports HTTP/2.

Step 2: Configure Nginx for HTTP/2

To enable HTTP/2, you need to add the http2 parameter to the listen directive in your SSL server block configuration. Do not enable HTTP/2 for non-SSL configurations, as browsers only support HTTP/2 over TLS/SSL.

  1. Open your Nginx configuration file for the site you wish to enable HTTP/2 on. This is typically located in /etc/nginx/sites-available/yourdomain.conf, or it might be directly in /etc/nginx/nginx.conf depending on your setup.
  2. Find your HTTPS server block, which should be listening on port 443 with SSL configuration.
  3. Add http2 to the listen directive:
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2 ipv6only=on;

    server_name yourdomain.com;

    # SSL configuration (ssl_certificate, ssl_certificate_key, etc.)

    # The rest of your server block...
}
  • Ensure that the listen directive includes both ssl and http2.
  • If you’re supporting IPv6, add http2 to the IPv6 listen directive as well.
#verify
curl -I --http2 https://example.com

HTTP/3

For those intrigued by the latest advancements in web technology, HTTP/3 represents the cutting edge of efficient, reliable, and secure internet communication. As the successor to HTTP/2, HTTP/3 further optimizes web performance by leveraging the QUIC transport protocol, which reduces latency, improves connection establishment times, and enhances security. This makes HTTP/3 particularly appealing to web developers, network administrators, and technology enthusiasts eager to push the boundaries of web performance and user experience. With its promise to significantly improve how we interact with the web, HTTP/3 is a topic of keen interest for anyone looking to stay at the forefront of internet technology.

Enable GZIP

This section details how to enhance your website’s performance by enabling GZIP compression in your Nginx server configuration. GZIP compression minimizes the size of the files served by your server, facilitating faster transfer speeds to the client’s browser, which is crucial for improving loading times and overall user experience.

To activate GZIP compression, you will need to edit the main Nginx configuration file, typically found at /etc/nginx/nginx.conf. Within the http block of this file, you should either add or confirm the presence of specific directives responsible for managing GZIP settings. These directives include turning GZIP on, setting the compression level to a recommended balance between efficiency and resource usage (level 6 is often a good compromise), and specifying the types of files to compress (commonly text and web application formats). Additionally, you can configure GZIP to work with various proxies and older HTTP versions, ensuring broad compatibility.

By implementing these settings, you not only enhance the efficiency of data transfer between your server and the client’s browser but also contribute to a smoother, faster browsing experience for your site visitors. This optimization is particularly beneficial for users on slower connections or mobile devices, where every kilobyte saved can significantly impact performance.

  1. Open your main Nginx configuration file, usually /etc/nginx/nginx.conf.
  2. Look for the http block and add or ensure the following directives are present to enable GZIP compression:

http {
    # Other http block directives...

    # Enable GZIP Compression
    gzip on;
    gzip_disable "msie6";  # Disable gzip for very old browsers

    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;  # Compression level, 1 is least compression and 6 is a good balance
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    # Rest of your http block configuration...
}

Final Nginx Config

fastcgi_cache_path /var/run/examplecom levels=1:2 keys_zone=examplecomcache:200m max_size=128m inactive=2h use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

server {

        server_name example.com;

        root /var/www/example.com;
        index index.php index.html index.htm index.nginx-debian.html;

        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

        set $skip_cache 0;

        # POST requests and url's with a query string should always skip cache
        if ($request_method = POST) {
          set $skip_cache 1;
        }

        if ($query_string != "") {
          set $skip_cache 1;
        }

        # Don't cache url's containing the following segments
        if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
          set $skip_cache 1;
        }

        # Don't use the cache for logged in users or recent commenters
        if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
          set $skip_cache 1;
        }

        client_max_body_size 25m;

        location / {
                try_files $uri $uri/ =404 /index.php?$args;
        }

        location ~ \.php$ {
          include snippets/fastcgi-php.conf;
          fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
          fastcgi_param HTTPS 1;


          fastcgi_cache examplecomcache;
          fastcgi_cache_valid 200 301 302 2h;
          fastcgi_cache_use_stale error timeout updating invalid_header http_500 http_503;
          fastcgi_cache_min_uses 1;
          fastcgi_cache_lock on;
          fastcgi_cache_bypass $skip_cache;
          fastcgi_no_cache $skip_cache;
          add_header X-FastCGI-Cache $upstream_cache_status;
        }

        location ~ /\.ht {
          deny all;
        }


    listen [::]:443 ssl http2 ipv6only=on; # managed by Certbot
    listen 443 ssl http2; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}


server {
    if ($host = example.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


        listen 80;
        listen [::]:80;

        server_name example.com;
    return 404; # managed by Certbot


}