(Last Updated On: June 19, 2018)

Are you a WordPress user who want to switch to WordPress Multisite Network setup – To host more than one domain on the same WordPress instance?. In this guide, I’ll take you through the steps to setup WordPress Multisite Network using Nginx as a web server and secure the setup with Letsencrypt SSL certificates on Ubuntu 18.04, Debian 9 and CentOS 7.

In a nutshell, this is a LEMP Stack with WordPress installed and secured with Letsencrypt. So we will start our installation by setting up LEMP stack on Ubuntu 18.04, Debian 9 and CentOS 7.

Installing MariaDB on CentOS 7 / Ubuntu 18.04 / Debian 9

This setup requires a database server. We will install and create a database to be used by WordPress. Refer to below guide on How to install MariaDB on these systems.

Install MariaDB 10.3 on Ubuntu 18.04 and CentOS 7

After the MariaDB server has been installed, proceed to create a database for WordPress. Login to your MariaDB database as root user and create a database for OCS:

$ mysql -u root -p
Enter password: 
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 19258
Server version: 10.3.7-MariaDB-1:10.3.7+maria~bionic-log mariadb.org binary distribution

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]> create database wp_db;
MariaDB [(none)]> grant all privileges on wp_db.* to wp_user identified by "strongpassword";
Query OK, 0 rows affected (0.000 sec)
MariaDB [(none)]> flush privileges;
Query OK, 0 rows affected (0.001 sec)

Once the database is ready, proceed to install nginx web server:

Installing Nginx on CentOS 7 / Ubuntu 18.04 / Debian 9

Nginx package on CentOS 7 is available on EPEL repository, you can install epel repository using the command:

$ sudo yum install epel-release

Then install nginx by running:

$ sudo yum -y install nginx

For Ubuntu 18.04 and Debian 9, you can install nginx from apt repository:

$ sudo apt-get install nginx

Starting nginx service on both methods is from systemd service manager.

$ sudo systemctl start nginx

Enable service to start on boot using:

$ sudo systemctl enable nginx

Installing php and php-fpm on CentOS 7 / Ubuntu 18.04 / Debian 9

The next phase is the installation of php and all required modules. Unlike Apache web server, Nginx does not contain native PHP processing. For that, we have to install PHP-FPM (FastCGI Process Manager).On Ubuntu and Debian, install php and php-fpm using the commands:

$ sudo apt-get install php \
php-{fpm,pear,cgi,common,mbstring,net-socket,gd,xml-util,mysql,gettext,bcmath}

The version of php that will be installed from this is v7.2. Php socket is located on /var/run/php/ directory. For version 7.2 of php, this is located on /var/run/php/php7.2-fpm.sock, you can confirm this on /etc/php/7.2/fpm/pool.d/www.conf. The service will be started by default, and its name is php7.2-fpm.service 

For CentOS 7, install php and required modules using:

# yum -y install php72w-{cli,common,fpm,gd,mbstring,mysql,odbc,pdo,xml,opcache}

The start php fpm, use:

# systemctl start php-fpm.service

On CentOS 7, the default user used to run php-fpm is apache. You can change it to nginx:

# vim /etc/php-fpm.d/www.conf
user = nginx
group = nginx

Service listen address is  127.0.0.1:9000

# grep "listen = 127.0.0.1:9000" /etc/php-fpm.d/www.conf 
listen = 127.0.0.1:9000

Download and Install WordPress

$ wget wordpress.org/latest.tar.gz
$ tar xvf latest.tar.gz
$ sudo mkdir /srv/wp.computingforgeeks.com
$ sudo mv wordpress/* /srv/wp.computingforgeeks.com/

Configure WordPress Database connection

# cd /srv/wp.computingforgeeks.com/
# cp wp-config-sample.php wp-config.php

Edit  wp-config.php

define('DB_NAME', 'wp_db');
define('DB_USER', 'wp_user');
define('DB_PASSWORD', 'strongpassword');

Change ownership of /srv/wp.computingforgeeks.com/ to web user:

# Ubuntu / Debian
$ sudo chown -R www-data:www-data /srv/wp.computingforgeeks.com/

# CentOS 7
$ sudo chown -R nginx:nginx /srv/wp.computingforgeeks.com/

Configure Nginx:

Copy the following configuration snippet to /etc/nginx/conf.d/wp-site.conf

##################################
# WORDPRESS NGINX CONFIGURATIONS
##################################

server {
        listen 80;
        root /srv/wp.computingforgeeks.com;
        server_name wp.computingforgeeks.com;
        return 301 https://$server_name$request_uri;
}

server {
    	listen 443 ssl http2;
        root /srv/wp.computingforgeeks.com;
	access_log /var/log/nginx/wp_client_access.log;
	error_log /var/log/nginx/wp_client_error.log;
        server_name wp.computingforgeeks.com;
	ssl_certificate /etc/letsencrypt/live/wp.computingforgeeks.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/wp.computingforgeeks.com/privkey.pem;

# Attempt to rewrite wordpress in sub directory 
rewrite ^/wp/([_0-9a-zA-Z-]+)/(xmlrpc\.php|wp-[0-9a-z-]+\.php) /wp/$2;
rewrite ^/wp/([_0-9a-zA-Z-]+)/(wp-(admin|content|includes).*) /wp/$2;


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


#############
# Specify a charset
############
        charset                         utf-8;

############
# GZIP
###########

        gzip                            off;

#############
# Add trailing slash to */wp-admin requests.
############

        rewrite /wp-admin$ $scheme://$host$uri/ permanent;


############
# this prevents hidden files (beginning with a period) from being served
############

location ~ /\. {
        access_log                      off;
        log_not_found                   off;
        deny                            all;
}

###########
# SEND EXPIRES HEADERS AND TURN OFF 404 LOGGING
###########

        location ~* ^.+.(xml|ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|css|rss|atom|js|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
        access_log                      off;
        log_not_found                   off;
        expires                         max;
}

############
# Pass uploaded files to wp-includes/ms-files.php.
############

#       rewrite                         /files/$ /index.php last;

if ($uri !~ wp-content/plugins) {
        rewrite /files/(.+)$ /wp-includes/ms-files.php?file=$1 last;
}

# Rewrite multisite in a subdirectory '.../wp-.*' and '.../*.php'.
# if (!-e $request_filename) {
#    rewrite ^/[_0-9a-zA-Z-]+(/wp-.*) $1 last;
#    rewrite ^/[_0-9a-zA-Z-]+.*(/wp-admin/.*\.php)$ $1 last;
#    rewrite ^/[_0-9a-zA-Z-]+(/.*\.php)$ $1 last;
#}

# Rewrite multisite '.../wp-.*' and '.../*.php'.
if (!-e $request_filename) {
    rewrite /wp-admin$ $scheme://$host$uri/ permanent;
    rewrite ^/[_0-9a-zA-Z-]+(/wp-.*) /wp$1 last;
    rewrite ^/[_0-9a-zA-Z-]+(/.*\.php)$ /wp$1 last;
}


############
# Pass all .php files onto a php-fpm or php-cgi server
############

location ~ \.php$ {

        # Try the files specified in order. In our case, try the requested URI and if
        # that fails, try (successfully) to pass a 404 error.
        # zero day exploit defense

        try_files                       $uri =404;

        # Include the fastcgi_params defaults provided by nginx

        include                         /etc/nginx/fastcgi_params;

        # The amount of time for upstream to wait for a fastcgi process to send data.
        # We keep this *extremely* high so that one can be lazy when remote debugging.

        fastcgi_read_timeout            3600s;
        
         # Buffer size for reading the header of the backend FastCGI process.
        # This defaults to the value of a single fastcgi_buffers, so does not
        # need to be specified in our case, but it's good to be explicit.

        fastcgi_buffer_size             128k;

        # The number and size of the buffers into which the reply from the FastCGI
        # process in the backend is read.
        #
        # 4 buffers at 128k means that any reply by FastCGI greater than 512k goes
        # to disk and replies under 512k are handled directly in memory.

        fastcgi_buffers                 4 128k;

        # SCRIPT_FILENAME is a required parameter for things to work properly,
        # but was missing in the default fastcgi_params on upgrade to nginx 1.4.
        # We define it here to be sure that it exists.

        fastcgi_param                   SCRIPT_FILENAME $document_root$fastcgi_script_name;


 # Use the upstream for php7.0-fpm that we defined in nginx.conf

        #fastcgi_pass                    unix:/run/php/php7.2-fpm.sock;
        fastcgi_pass                    127.0.0.1:9000;

        # And get to serving the file!

        fastcgi_index                   index.php;
}


############
# ROBOTS
###########

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


############
# RESTRICTIONS
############

# Deny access to any files with a .php extension in the uploads directory
# Works in sub-directory installs and also in multisite network
# Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
location ~* /(?:uploads|files)/.*\.php$ {
 deny all;
}

}

Reme,ber to replace wp.computingforgeeks.com with your valid domain name,/srv/wp.computingforgeek.com with your web root.

Generate Letsencryt SSL certificate for the domain

Once you have modified nginx with correct settings, proceed to request for Letsencrypt certificate that we’ll use to secure the main site. Download and install certbot-auto client to use:

# wget https://dl.eff.org/certbot-auto -P /usr/local/bin
# chmod a+x /usr/local/bin/certbot-auto

Open http and https ports on the firewall:

# For CentOS
firewall-cmd --add-service={http,https} --permanent
firewall-cmd --reload

# For Ubuntu/Debian ufw
ufw allow http
ufw allow https

Request for certificate

# export DOMAIN='wp.computingforgeeks.com
# export EMAIL="[email protected]"
# certbot-auto certonly --standalone -d $DOMAIN --preferred-challenges http \
--agree-tos -n -m $EMAIL --keep-until-expiring

You should get output like this:

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator standalone, Installer None
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for wp.computingforgeeks.com
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/wp.computingforgeeks.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/wp.computingforgeeks.com/privkey.pem
   Your cert will expire on 2018-09-17. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot-auto
   again. To non-interactively renew *all* of your certificates, run
   "certbot-auto renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

Confirm that certs we indeed generated:

# ls -1 /etc/letsencrypt/live/wp.computingforgeeks.com/
cert.pem
chain.pem
fullchain.pem
privkey.pem
README

Make sure you modify nginx configuration ssl section to point to correct path of letsencrypt private key and certificate. Restart nginx for the changes to be affected:

$ nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo systemctl restart nginx

Configure WordPress

Open http:domain to start wordpress installation. For me, this will be http://wp.computingforgeeks.com. You should get initial installation page:

Provide required information and click on “Install WordPress” button. If all goes well, you should get login page.

Click Log in and provide username and password.

On Login, you should get to wordpress Admin dashboard.

Configure WordPress Multisite

For WordPress multisite to work, you need to first enable it on wordpress configuration file:

# vim /srv/wp.computingforgeeks.com/wp-config.php

Add the following content before the line /* That's all, stop editing! Happy blogging. */

define( 'WP_ALLOW_MULTISITE', true );

Restart Nginx service:

$ sudo systemctl restart nginx

Relogin to WordPress Admin Page and go to:

Tools > Network Setup

Choose whether to use Sub-domains or sub-directories to host other sites.

Enabling WordPress Network

Paste the given configuration snippet on /srv/wp.computingforgeeks.com/wp-config.php, just before /* That's all, stop editing! Happy blogging. */

define('MULTISITE', true);
define('SUBDOMAIN_INSTALL', true);
define('DOMAIN_CURRENT_SITE', 'wp.computingforgeeks.com');
define('PATH_CURRENT_SITE', '/');
define('SITE_ID_CURRENT_SITE', 1);
define('BLOG_ID_CURRENT_SITE', 1);

Restart nginx:

# systemct restart nginx

Relogin to start using WordPress Network Multisite feature. You should see new Network Admin Menu.

You can start adding websites to your WordPress Network by navigating to Sites > Add New

In our next guide, I’ll cover how to Add websites to wordpress Multisite Network Setup.