Categories
Tutorials

How to run WordPress behind Nginx with SSL and Caching on Ubuntu 20.04

Recently I’ve migrated this site from WordPress.com to a self-deployed machine on AWS. Up until now, I’ve never set up a WordPress site which to my surprise, wasn’t that easy. A lot of stuff to think about, a lot of moving parts to manage.

In this post, I’ll share my experience. I did all of this on an instance running Ubuntu 20.04 on AWS, but this can be implemented on any cloud provider. The entire setup, and what you’ll get from reading this post includes a WordPress site, running on a MariaDB database behind an Nginx with SSL encryption and FastCGI caching.

Setup steps will be as follow:

  1. Install Nginx, MariaDB, and PHP via APT
  2. Creating a database and a user account for WordPress
  3. Download and configuring WordPress
  4. Configure Nginx as a gateway to WordPress
  5. Settings up SSL support using certbot (Let’s Encrypt) – Optional
  6. Setting up FastCGI caching in Nginx – Optional

So let’s start.

Installing APT packages

We’ll start by installing everything we can with pre-built APT packages. That will include, Nginx, MariaDB, PHP-FPM, and some required modules.

sudo apt update

sudo apt install -y mariadb-server mariadb-client nginx php-fpm php-common php-mbstring php-xmlrpc php-soap php-gd php-xml php-intl php-mysql php-cli php-ldap php-zip php-curl

Creating a Database and user account in MariaDB

Now that we have MariaDB installed, we need to configure it. To do so, we’ll run:

sudo mysql_secure_installation

First question is for the current root password, because that is a fresh installation, there is none. So just press enter.

The second question is for setting the root password. We will not use the root user, and by default, MariaDB allows the root user to connect only by unix_socket (locally), so you can go ahead and press enter here too. All other questions after that answer with Y.

Now that we have MariaDB up and running, we need to prepare an empty database and a fresh user account for WordPress to use.

Login to the server by running

mysql

Inside the MariaDB console, run these commands:

MariaDB [(none)]> CREATE DATABASE new_wp;
MariaDB [(none)]> GRANT ALL PRIVILEGES ON new_wp.* TO wpuser@localhost IDENTIFIED BY 'myp@Ssw0Rd';
MariaDB [(none)]> FLUSH PRIVILEGES;
MariaDB [(none)]> EXIT;

We run mysql command to log in to the database server, then, create a database with the name we want (“new_wp” in the example). Then, create a user and grant it all privileges to the new database. In this example, username wpuser with the password “myp@Ssw0Rd”. Pay attention for this user can only log in to the server from the same machine (“wpuser@localhost”), that is because WordPress will run on the same machine.

Download and configure WordPress

We have our Database backend ready, now we’ll download and configure WordPress to use it.

wget https://wordpress.org/latest.tar.gz
tar -xzvf latest.tar.gz

These two commands will download and extract the latest WordPress release to a directory called wordpress in the same directory you are at.

In the directory you’ll find a file called wp-config-sample.php, this is a template configuration file for WordPress, we can rename and use it in order to save some time.

Rename it with:

mv wp-config-sample.php wp-config.php

Now we need to open wp-config.php with our favorite text editor and edit the values in it. Most of them are DB connection related, like user name, password, DB name and so on… there are comments inside the file that will help and the fields are really self explanatory. Also, there is the official page on how to edit this file.

In short, these are the lines we care about:

// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'database_name_here' );
/** MySQL database username */
define( 'DB_USER', 'username_here' );
/** MySQL database password */
define( 'DB_PASSWORD', 'password_here' );
/** MySQL hostname */
define( 'DB_HOST', 'localhost' );

See, self explanatory… After the DB lines, comes the secret keys section, for this we have a handy online random generator you can use, just replace the corresponding lines in the file with those of the generator.

When finished, save and close the file.

Now let’s move it to a more appropriate location and set permissions:

sudo cp -R wordpress/ /var/www/html/example.com
sudo chown -R www-data:www-data /var/www/html/example.com
sudo chmod -R 775 /var/www/html/example.com

WordPress is ready, all we left to do is to set up Nginx.

Configure Nginx

First step is to delete the default server coming pre-configured with nginx, so ours will be the only one.

sudo rm /etc/nginx/sites-enabled/default

Now, create a virtual host file to define our site:

sudo vim /etc/nginx/sites-enabled/example.com.conf

In this file we put all configuration and routing for the site:

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

  root /var/www/html/example.com;
  index  index.php;
  server_name example.com;

  error_log /var/log/nginx/example.com_error.log;
  access_log /var/log/nginx/example.com_access.log;

  location = /favicon.ico {
    log_not_found off;
    access_log off;
  }

  location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
  }

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

  location ~ \.php$ {    
    include snippets/php.conf;
  }
}

You’ll notice this file includes another file from snippets/php.conf. This is the helper file I’ve created in order to keep the code clean.

Create the file:

sudo vim /etc/nginx/snippets/php.conf

And paste this code inside:

include snippets/fastcgi-php.conf;
fastcgi_intercept_errors on;
fastcgi_pass             unix:/var/run/php/php-fpm.sock;
fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;

This snippet includes all the configuration needed for Nginx to run WordPress via the PHP interpreter.

Note: You can include snippet/php.conf data inside the location section in the example.com.conf file – it is the same as including it.

So now, the basic Nginx configuration is ready and you can run a config check and restart Nginx for all the changes to load.

sudo nginx -t
sudo systemctl restart nginx

Now open your browser and direct it to the server IP to complete WordPress setup using the UI (Open http://SERVER_IP/).

You should see the configuration screen. Fill it up with your info and you have the basic setup ready.

wordpress install screen

You now have WordPress running on your server, with Nginx as a front server and MariaDB as a backend database. The next sections are optional but very recommended to get the fast, and secured setup.

Setting up SSL

In order to secure communication to the site via SSL, we’ll use an SSL certificate from Let’s Encrypt.

To do that, we’ll use the certbot tool with it’s Nginx plugin. Let’s install it:

sudo apt install certbot python3-certbot-nginx

Now we’ll run certbot and ask it to verify us as example.com and issue a certificate for us to use within Nginx configuration:

sudo certbot --nginx -d example.com

If all if right, certbot will generate our certificate and certificate key and save them to 2 different files.

/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem

Note that certbot will renew the certificates automatically. But just to be on the safe side, you can do a dry run and see the procedure is running with this command:

sudo certbot renew --dry-run

And to monitor the status of the renew timer with:

sudo systemctl status certbot.timer

We’ll use these files and their paths inside Nginx in order for it to secure our site communication with them. But why stop there? If we already have SSL support, let’s move all traffic to it. Remember /etc/nginx/sites-enabled/example.com.conf ? Let’s edit it.

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

  server_name example.com;

  return 301 https://example.com$request_uri;
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  root /var/www/html/example.com;
  index  index.php;
  server_name example.com;

  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  error_log /var/log/nginx/example.com_error.log;
  access_log /var/log/nginx/example.com_access.log;

  location = /favicon.ico {
    log_not_found off;
    access_log off;
  }

  location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
  }

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

  location ~ \.php$ {     
    include snippets/php.conf;
  }
}

You can see here that we’ve split the single server we had to two separate servers, one on port 80 – no SSL, and the other on port 443 – with SSL. In the insecure server on port 80 we do not do a thing except of redirecting the user to the secure server on port 443.

On the secure server, we added the generated certificates paths in order to match our domain. Load WordPress again and you should be redirected to https://example.com and see the lock sign in your browser right next to the address.

Go faster with FastCGI caching

Nginx and dedicated infrastructure and database are great. WordPress will run its code a lot faster than on any other 5$/month hosting service. But you know how we make the code run even fast? NOT RUN IT AT ALL 🙂

Yes, caching is great, the fasts way to serv web pages is to just get them from memory. In order to use caching, we will use FastCGI caching mechanism of Nginx.

Add these two lines to the top of the virtual host file:

fastcgi_cache_path /etc/nginx/cache levels=1:2 keys_zone=WP:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

These lines define a cache storage under /etc/nginx/cache with the identity WP. It’s limited to 100MB of storage space and every page cached in there will live 60 minutes.

We also define the cache key, which represent the string of which each request is saved by. This should be a unique string per URL requested. In this case, we take the scheme(HTTPS), the request method(GET), the host and the URI (example.com/path-to-post). In the background, nginx will generate a single string from those params and will run MD5 on them, the result will be the file name it will use to save the generated page.

But that’s just the cache bucket defenition, now we need to actually tell nginx in what locations to use it. So inside snippets/php.conf add these lines:

fastcgi_cache WP;
fastcgi_cache_valid 200 60m;

This tells Nginx to cache all requests with status code 200, for 60 minutes in cache bucket named WP.

Now, problem with cache is, that there might be some pages we don’t want to cache. For example, admin pages (the whole dashboard), API calls, POST requests, etc… In order to avoid them, add the following to the server section in the file

set $skip_cache 0;

  # POST requests and urls with a query string should always go to PHP
  if ($request_method = POST) {
    set $skip_cache 1;
  }
  if ($query_string != "") {
    set $skip_cache 1;
  }

  # Don't cache uris 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;
  }

And to the snippet/php.conf add this:

fastcgi_cache_methods GET HEAD;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;

These are the most common exceptions for caching. we set $skip_cache variable to 1, when we don’t want to cache the request. you can remove or add rules as you like.

Another addition you’d like to have under the server section (in most cases) is:

add_header X-Cache $upstream_cache_status;
fastcgi_cache_use_stale error timeout invalid_header http_500;
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

add_header – adds the X-Cache header to the response. This is great for debugging as you can investigate each request and see if it returned from cache or not.

fastcgi_cache_use_stale – Tells Nginx in which errors returned by PHP it can use a stale cache.

fastcgi_ignore_headers – Tells Nginx to ignore these headers returned from WordPress. This is here because we want to override the theme running on WordPress from dictating what to cache and what not to cache. If not added, some themes return headers that will prevent Nginx from caching the response.

Final Nginx config file should look like this:

fastcgi_cache_path /etc/nginx/cache levels=1:2 keys_zone=WP:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

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

  server_name example.com;

  return 301 https://example.com$request_uri;
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  root /var/www/html/example.com;
  index  index.php;
  server_name example.com;

  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  error_log /var/log/nginx/example.com_error.log;
  access_log /var/log/nginx/example.com_access.log;

  add_header X-Cache $upstream_cache_status;
  fastcgi_cache_use_stale error timeout invalid_header http_500;
  fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

  set $skip_cache 0;

  # POST requests and urls with a query string should always go to PHP
  if ($request_method = POST) {
    set $skip_cache 1;
  }
  if ($query_string != "") {
    set $skip_cache 1;
  }

  # Don't cache uris 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;
  }

  location = /favicon.ico {
    log_not_found off;
    access_log off;
  }

  location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
  }

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

  location ~ \.php$ {   
    include snippets/php.conf;
  }
}

And final snippets/php.conf file should like like this:

include snippets/fastcgi-php.conf;
fastcgi_intercept_errors on;
fastcgi_pass             unix:/var/run/php/php-fpm.sock;
fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_cache_methods GET HEAD;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache WP;
fastcgi_cache_valid 200 60m;

Conclusion

That’s it, you have a WordPress site, saving it’s data to MariaDB, running behind Nginx with SSL encryption and served quickly with caching.

I hope this post helped you in your journey to a self-deployed WordPress site, if it did, or if you got stuck in the process, I’d love to hear about your experience in the comments.

Leave a Reply

Your email address will not be published. Required fields are marked *