Setting up Wordpress with Nginx, Varnish, Apache, PHP 7 and MariaDB

Published: 2021-01-18 | Last Updated: 2021-02-02 | ~17 Minute Read

Table of Contents

Context

I recently had to do a setup similar to this and I thought I’d share the process with you as well. This is not a full tutorial, it’s more of an overview but I think it’ll show the general idea behind how the setup was done.

System specs

These were the specs of the Debian server that I was working with.

ubuntu@server:~$ free -h
              total        used        free      shared  buff/cache   availabl
Mem:          2.0Gi        42Mi       1.8Gi        10Mi       126Mi       1.8G
Swap:            0B          0B          0B

ubuntu@server:~$ uname -a
Linux server 4.19.0-8-cloud-amd64 #1 SMP Debian 4.19.98-1 (2020-01-26) x86_64 GNU/Linux

ubuntu@server:~$ cat /proc/cpuinfo | grep model
model           : 85
model name      : Intel(R) Xeon(R) Gold 6140 CPU @ 2.30GHz

ubuntu@server:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 10 (buster)
Release:        10
Codename:       buster

Requirements

This setup had a few specific requirements:

Web-Flow Diagram

This is what it had to look like from a bird’s eye point of view:

 #########
 #       #
 # Nginx # - - - - -
 #       #         |
 #########         |
     |             |
     v             |
 ###########       |
 #         #       |
 # Varnish #       | In case of
 #         #       | varnish 
 ###########       | failure
     |             |
     v             |
 ##########        |
 #        #        |
 # Apache # <-  -  -
 #        #
 ##########
      |
      | - - - - (Connection via TCP/IP Sockets)
      v
 ###########
 #         #
 # PHP-FPM #
 #         #
 ###########
      |
      | - - - - (Connection via Unix Sockets)
      v
 ###########
 #         #
 # MariaDB #
 #         #
 ###########

Software Installation

Installation of Nginx

To install Nginx I added the official repositories to the /etc/apt/sources.list.d/nginx.list file for the debian version installed:

deb http://nginx.org/packages/debian/ buster nginx
deb-src http://nginx.org/packages/debian/ buster nginx

Installed Nginx version:

ubuntu@server:~$ sudo nginx -v
nginx version: nginx/1.18.0

Installation of Apache

I then installed apache, version installed:

ubuntu@server:~$ sudo apache2 -v
Server version: Apache/2.4.38 (Debian)
Server built:   2019-10-15T19:53:42

Installation of Varnish

Installed Varnish, version installed:

root@server:/etc/init.d# varnishd -V
varnishd (varnish-6.1.1 revision efc2f6c1536cf2272e471f5cff5f145239b19460)
Copyright (c) 2006 Verdens Gang AS
Copyright (c) 2006-2015 Varnish Software AS

Installation of PHP

Installed PHP, version installed:

root@server:/etc/init.d# php -v
PHP 7.3.19-1~deb10u1 (cli) (built: Jul  5 2020 06:46:45) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.19, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.3.19-1~deb10u1, Copyright (c) 1999-2018, by Zend Technologies

Installation of PHP-FPM

Installed PHP-FPM, version installed:

root@server:/etc/init.d# /etc/init.d/php7.3-fpm status
● php7.3-fpm.service - The PHP 7.3 FastCGI Process Manager
Jul 30 00:27:20 server systemd[1]: Starting The PHP 7.3 FastCGI Process Manager...
Jul 30 00:27:20 server systemd[1]: Started The PHP 7.3 FastCGI Process Manager.

Domain Configuration

The domain used is domain.com

Set an A record for the domain pointing to the server IP so we can access the via domain name.

Software Configuration

Nginx Configuration

Updated the default virtual host file /etc/nginx/conf.d/default.conf and named it domain.com.conf

Updated the contents to include PHP redirection and logging as follows:

server {
    listen       80;
    server_name  localhost;

    access_log  /var/log/nginx/wordpress.access.log  main;
    error_log  /var/log/nginx/wordpress.error.log  crit;

    location / {
        root   /usr/share/nginx/html;
        index  index.php index.html index.htm;
    }

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:8080
    #
    location ~ \.php$ {
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass   http://127.0.0.1:8080;
    }

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    location ~ /\.ht {
        deny  all;
    }
}

With those updates we can now see the logs here:

root@server:/var/log/nginx# ls
wordpress.access.log  wordpress.error.log

Apache Configuration

I did a couple of configuration changes here:

Updated the contents to listen on port 8080 on localhost as follows:

<VirtualHost 127.0.0.1:8080>
        # The ServerName directive sets the request scheme, hostname and port that
        # the server uses to identify itself. This is used when creating
        # redirection URLs. In the context of virtual hosts, the ServerName
        # specifies what hostname must appear in the request's Host: header to
        # match this virtual host. For the default virtual host (this file) this
        # value is not decisive as it is used as a last resort host regardless.
        # However, you must set it for any further virtual host explicitly.
        ServerName backend

        ServerAdmin webmaster@localhost
        DocumentRoot /usr/share/nginx/html

        # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
        # error, crit, alert, emerg.
        # It is also possible to configure the loglevel for particular
        # modules, e.g.
        #LogLevel info ssl:warn

        SetEnvIf X-Forwarded-For "^.*\..*\..*\..*" forwarded
        LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
        LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" forwarded
        ErrorLog ${APACHE_LOG_DIR}/wordpress.error.log
        CustomLog ${APACHE_LOG_DIR}/wordpress.access.log combined env=!forwarded
        CustomLog ${APACHE_LOG_DIR}/wordpress.access.log forwarded env=forwarded

        # For most configuration files from conf-available/, which are
        # enabled or disabled at a global level, it is possible to
        # include a line for only one particular virtual host. For example the
        # following line enables the CGI configuration for this host only
        # after it has been globally disabled with "a2disconf".
        Include conf-available/php7.3-fpm.conf
</VirtualHost>

PHP-FPM listening status:

root@server:~# ss -tulpn | grep fpm
tcp   LISTEN 0      128                           127.0.0.1:8081        0.0.0.0:*                                                                                users:(("php-fpm7.3",pid=9670,fd=9),("php-fpm7.3",pid=9669,fd=9),("php-fpm7.3",pid=9668,fd=7))

Current webflow as seen from logs:

This is the output that we get from the updated log configuration we implemented, we can see the real IP from the client in the Apache log even though it’s behind Nginx.

root@server:~# tail -1 /var/log/nginx/wordpress.access.log
xxx.xxx.xxx.xxx - - [30/Jul/2020:03:59:16 +0000] "GET / HTTP/1.1" 200 21310 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0" "-"

root@server:~# tail -1 /var/log/apache2/wordpress.access.log 
xxx.xxx.xxx.xxx - - [30/Jul/2020:03:59:16 +0000] "GET /index.php HTTP/1.0" 200 21310 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0"

Varnish Configuration

The configuration changes done for Varnish are the following:

This is what the Varnish service file looks like:

root@server:~# cat /etc/systemd/system/varnish.service
[Unit]
Description=Varnish HTTP accelerator
Documentation=https://www.varnish-cache.org/docs/6.1/ man:varnishd

[Service]
Type=simple
LimitNOFILE=131072
LimitMEMLOCK=82000
ExecStart=/usr/sbin/varnishd -j unix,user=vcache -F -a 127.0.0.1:6081 -T localhost:6082 -f /etc/varnish/wordpress-cache.vcl -S /etc/varnish/secret -s malloc,256m
ExecReload=/usr/share/varnish/varnishreload
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
PrivateDevices=true

[Install]
WantedBy=multi-user.target

This is what the Varnish configuration file looks like:

root@server:/etc/varnish# cat wordpress-cache.vcl
#
# This is an example VCL file for Varnish.
#
# It does not do anything by default, delegating control to the
# builtin VCL. The builtin VCL is called when there is no explicit
# return statement.
#
# See the VCL chapters in the Users Guide at https://www.varnish-cache.org/docs/
# and https://www.varnish-cache.org/trac/wiki/VCLExamples for more examples.

# Marker to tell the VCL compiler that this VCL has been adapted to the
# new 4.0 format.
vcl 4.0;

include "hit-miss.vcl";
import std;

# Default backend definition. Set this to point to your content server.
backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

sub vcl_recv {
    # Happens before we check if we have this in cache already.
    #
    # Typically you clean up the request here, removing cookies you don't need,
    # rewriting the request, etc.
}

sub vcl_backend_response {
    # Happens after we have read the response headers from the backend.
    #
    # Here you clean the response headers, removing silly Set-Cookie headers
    # and other mistakes your backend does.
}

sub vcl_deliver {
    # Happens when we have all the pieces we need, and are about to send the
    # response to the client.
    #
    # You can do accounting or modifying the final object here.
}

Nginx Updated Configuration

With the above working as expected, it was time to move on to the next step:

Nginx updated configuration:

root@server:/etc/varnish# cat /etc/nginx/conf.d/wordpress.conf
upstream backend {
    server 127.0.0.1:6081 max_fails=1 fail_timeout=1s;
server 127.0.0.1:8080 backup;
}

server {
    listen       80;
    server_name  localhost;

    access_log  /var/log/nginx/wordpress.access.log  main;
    error_log  /var/log/nginx/wordpress.error.log  crit;

    location / {
        root   /usr/share/nginx/html;
        index  index.php index.html index.htm;
    }

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:8080
    #
    location ~ \.php$ {
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass   http://backend;
    }

    # Access PHP-FPM /status page from localhost for testing
    #
    location ~ ^/(status|ping)$ {
            allow 127.0.0.1;
    deny all;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_index index.php;
            include fastcgi_params;
            fastcgi_pass 127.0.0.1:8081;
    }

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    location ~ /\.ht {
        deny  all;
    }
}

Current webflow as seen from logs:

With those updates we now have the following in the logs whenever we get a request:

From the client:

bash-4.3$ curl -I http://domain.com
HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Thu, 30 Jul 2020 06:09:50 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 21081
Connection: keep-alive
Vary: Accept-Encoding
X-Varnish: 32777 32775
Age: 55
Via: 1.1 varnish (Varnish/6.1)
Accept-Ranges: bytes

From the server:

root@server:/etc/varnish# varnishlog | grep -i cache
-   ReqHeader      x-cache: hit
-   ReqUnset       x-cache: hit
-   ReqHeader      x-cache: hit cached

root@server:/etc/varnish# tail -1 /var/log/nginx/wordpress.access.log 
xxx.xxx.xxx.xxx - - [30/Jul/2020:06:09:50 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.71.0" "-"

root@server:/etc/varnish# tail -1 /var/log/apache2/wordpress.access.log 
xxx.xxx.xxx.xxx, 127.0.0.1 - - [30/Jul/2020:06:08:55 +0000] "GET /index.php HTTP/1.1" 200 21081 "-" "curl/7.71.0"

MariaDB Configuration

The changes done to the MariaDB info were pretty straight forward;

MariaDB listening status:

ubuntu@server:~$ ss -a | grep mysql
u_str   LISTEN     0        80                          /run/mysqld/mysqld.sock 3455
tcp     LISTEN     0        80                                                *:mysq

Firewall Configuration

This is the first time I use nftables and I enjoyed using it a lot.

Nftables configuration file:

root@server:~# cat /etc/nftables.conf 
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
        chain input {
                type filter hook input priority 0;

                # Accept loopback
                iifname lo accept;

                # Accept established/related
                ct state established,related accept;

                # Accept SSH / HTTP / HTTPs
                tcp dport {22, 80, 443} counter accept;

                # Drop all traffic
                policy drop;
        }
        chain forward {
                type filter hook forward priority 0;

                # Drop all traffic
                policy drop;
        }
        chain output {
                type filter hook output priority 0;

                # Accept all traffic
                policy accept;
        }
}

Github Configuration & Wordpress Deployment

This is how I implemented the website to be able to be easily deployed & updated via Github

Backup Configuration

I created a simple backup script that took care of the following:

All files related to backups will be stored under /root with the following directory structure:

├── wordpress-backup
│   ├── full-backups
│   ├── incremental-backups
│   └── logs
└── wordpress-backup.sh

Full backup implementation

Partial backup implementation

Github backup implementation

Script source code

root@server:~# cat wordpress-backup.sh 
#! /bin/bash
# Script to backup Wordpress installation on this server

# Define log file variable
BACKUP_EXEC_LOG_FILE=/root/wordpress-backup/logs/backup-execution_`date +%m-%d-%y_%H-%M-%S`.log

# Catch stdout and stderr, and log  execution of script
exec 1>> "$BACKUP_EXEC_LOG_FILE"
exec 2>&1

# This script will create three different backups
# 1. An incremental backup with rsync
# 2. A full backup in the .tar.gz format with compression
# 3. Trigger git routines to backup externally to the git repository linked to the site
# All files will be backed up to the /root/wordpress-backup folder

# Define script functions

function checkExitStatus {
# Check if the command exit status is not equal to 0
if [ $? != 0 ]
then
        # Echo an error message and exit
        echo "$DB_MSG"; exit
fi
}

# Define script variables
BACKUP_EXEC_LOGS_DIR=/root/wordpress-backup/logs
DATE=`date +%m-%d-%y_%H-%M-%S`
BACKUP_SOURCE=/usr/share/nginx/html/wordpress/
FULL_BACKUP_DIR=/root/wordpress-backup/full-backups
INCREMENTAL_BACKUP_DIR=/root/wordpress-backup/incremental-backups/
DB_BACKUP_FILE=/root/wordpress-backup/db-backup.sql
DB_MSG="There was an error creating the DB backup file. Backup not executed, exiting. mysqldump exit status $?"
DB_MSG1="Backup DB file does not exist yet, unable to continue. Backup not executed, exiting"
TAR_MSG="There was an error in creating the tar archive for the backup. Backup not executed, exiting. tar exit status 
RSYNC_MSG="There was an error in creating the partial backup. Exiting. rsync exit status $?"
COMMIT_MSG="Daily backup"
NO_COMMIT_MSG="No changes to commit, nothing to push"

# Full backup

# Backup the mariadb database
mysqldump wpdb --single-transaction > $DB_BACKUP_FILE

# Call exit status function
checkExitStatus

# Backup the /wordpress web directory
# Check that the tmp-db-backup file exists before proceeding
if [ -f $DB_BACKUP_FILE ]
then 
        # Create the backup by using the tar command
        tar -cvzpf $FULL_BACKUP_DIR/full-wordpress-backup_$DATE.tar.gz $BACKUP_SOURCE $DB_BACKUP_FILE

        # Call exit status function
        checkExitStatus

        # Delete the DB_BACKUP_FILE from the /root/wordpress-backups directory
        rm $DB_BACKUP_FILE
else
        # Echo an error message and exit
        echo $DB_MSG1 ; exit
fi

# Partial Backup

# Use rsync to create an incremental backup
rsync -ahv --delete $BACKUP_SOURCE $INCREMENTAL_BACKUP_DIR

# Call exit status function
checkExitStatus

# Github repository backup

# Change the working directory to $BACKUP_SOURCE as this is the local git repository
cd $BACKUP_SOURCE

# Checkout the backup branch
git checkout backup

if [ `git diff | wc -l` != 0 ]
then
        # Commit all changes
        git commit -am "$COMMIT_MSG"

        # Push staged changes to github
        git push origin backup
else
        echo $NO_COMMIT_MSG
fi

# Checkout the master branch
git checkout master

SSL Configuration

I setup a self signed certificate at first and then implemented Let’s Encrypt for the SSL certificate and the configuration overview is as follows;

Nginx configuration

Nginx updated configuration

The updated Nginx configuration file:

# Configure backend
upstream backend {
        server 127.0.0.1:6081 max_fails=1 fail_timeout=1s;
        server 127.0.0.1:8080 backup;
}

# Configure https redirect
server {
        listen 80;
        server_name domain.com;
        return 301 https://$server_name$request_uri;
}

# Configure HTTPs connections
server {
        listen 443 ssl;
        server_name domain.com;
        root /usr/share/nginx/html/wordpress;
        index index.php;

        # Configure Logging
        access_log  /var/log/nginx/wordpress.access.log  main;
        error_log  /var/log/nginx/wordpress.error.log  crit;

        # SSL settings
        ssl_certificate /etc/nginx/ssl/nginx-selfsigned.crt;
        ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers HIGH:!aNULL:MD5;


        # Configure requests for requests to /
        location = / {
                # Pass connections to the backend
                proxy_pass          http://backend;

                # Rewrite apache output header 'location' value from 'http://' to 'https://'
                proxy_redirect      http://             $scheme://;

                # Set header 'Host' to 'xxx`
                proxy_set_header    Host                $host;

                # Pass the address of the client to the backend
                proxy_set_header    X-Real-IP           $remote_addr;

                # Set header 'X-Forwarded-For' to the remote address. Apache logs client and proxy address (127.0.0.1)
                proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;

                # set header 'X-Forwarded-Proto' 'https'
                proxy_set_header    X-Forwarded-Proto   $scheme;
        }

        # Redirect all php requests to backend
        location ~ \.php$ {
                proxy_pass          http://backend;
                proxy_redirect      http://             $scheme://;
                proxy_set_header    Host                $host:$server_port;
                proxy_set_header    X-Real-IP           $remote_addr;
                proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
                proxy_set_header    X-Forwarded-Proto   $scheme;
        }

        # Request that static objects are stored client side as long as possible
        location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
                expires max;
                log_not_found off;
        }

        # Block access to the .htaccess file that wordpress creates
        location ~ /\.ht {
                deny all;
        }
}

Curl output from a client with the above configuration:

bash-4.3$ curl -Ik https://domain.com
HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Sun, 02 Aug 2020 06:56:19 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 6996
Connection: keep-alive
Link: <https://domain.com/index.php?rest_route=/>; rel="https://api.w.org/"
Vary: Accept-Encoding
X-Varnish: 98732 941
Age: 58
Via: 1.1 varnish (Varnish/6.1)
Accept-Ranges: bytes

The snippets that were added to the wp-config.php file:

// If the X_FORWARDED_FOR header is present in the request, Get a comma delimited list of IPs from the X_FORWARDED_FOR header and set the remote address to the first IP in the array
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
    $_SERVER['REMOTE_ADDR'] = $ips[0];
}

// If the the X_FORWARDED_PROTO header is present in the request and the value is https, set the uri to use https
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
    if ($_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
        $_SERVER['HTTPS'] = 'on';
    }
}

Let’s encrypt configuration

This is the setup I did for Let’s encrypt on the server:

Curl output from a client with the new letsencrypt certificate

bash-4.3$ curl -I https://domain.com
HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Sun, 02 Aug 2020 08:06:16 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 6996
Connection: keep-alive
Link: <https://domain.com/index.php?rest_route=/>; rel="https://api.w.org/"
Vary: Accept-Encoding
X-Varnish: 98827
Age: 0
Via: 1.1 varnish (Varnish/6.1)
Accept-Ranges: bytes

I did some research for this setup and learned a few things along the way. Here are some of the links that I thought were most helpful during the research.