You can support us by downloading this article as PDF from the Link below. Download the guide as PDF

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

[pastacode lang=”bash” manual=”%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%0A%23%20WORDPRESS%20NGINX%20CONFIGURATIONS%0A%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%0A%0Aserver%20%7B%0A%20%20%20%20%20%20%20%20listen%2080%3B%0A%20%20%20%20%20%20%20%20root%20%2Fsrv%2Fwp.computingforgeeks.com%3B%0A%20%20%20%20%20%20%20%20server_name%20wp.computingforgeeks.com%3B%0A%20%20%20%20%20%20%20%20return%20301%20https%3A%2F%2F%24server_name%24request_uri%3B%0A%7D%0A%0Aserver%20%7B%0A%20%20%20%20%09listen%20443%20ssl%20http2%3B%0A%20%20%20%20%20%20%20%20root%20%2Fsrv%2Fwp.computingforgeeks.com%3B%0A%09access_log%20%2Fvar%2Flog%2Fnginx%2Fwp_client_access.log%3B%0A%09error_log%20%2Fvar%2Flog%2Fnginx%2Fwp_client_error.log%3B%0A%20%20%20%20%20%20%20%20server_name%20wp.computingforgeeks.com%3B%0A%09ssl_certificate%20%2Fetc%2Fletsencrypt%2Flive%2Fwp.computingforgeeks.com%2Ffullchain.pem%3B%0A%20%20%20%20%20%20%20%20ssl_certificate_key%20%2Fetc%2Fletsencrypt%2Flive%2Fwp.computingforgeeks.com%2Fprivkey.pem%3B%0A%0A%23%20Attempt%20to%20rewrite%20wordpress%20in%20sub%20directory%20%0Arewrite%20%5E%2Fwp%2F(%5B_0-9a-zA-Z-%5D%2B)%2F(xmlrpc%5C.php%7Cwp-%5B0-9a-z-%5D%2B%5C.php)%20%2Fwp%2F%242%3B%0Arewrite%20%5E%2Fwp%2F(%5B_0-9a-zA-Z-%5D%2B)%2F(wp-(admin%7Ccontent%7Cincludes).*)%20%2Fwp%2F%242%3B%0A%0A%0Alocation%20%2F%20%7B%0A%20%20%20%20index%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20index.php%20index.html%3B%0A%20%20%20%20try_files%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%24uri%20%24uri%2F%20%2Findex.php%3F%24args%3B%0A%7D%0A%0A%0A%23%23%23%23%23%23%23%23%23%23%23%23%23%0A%23%20Specify%20a%20charset%0A%23%23%23%23%23%23%23%23%23%23%23%23%0A%20%20%20%20%20%20%20%20charset%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20utf-8%3B%0A%0A%23%23%23%23%23%23%23%23%23%23%23%23%0A%23%20GZIP%0A%23%23%23%23%23%23%23%23%23%23%23%0A%0A%20%20%20%20%20%20%20%20gzip%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20off%3B%0A%0A%23%23%23%23%23%23%23%23%23%23%23%23%23%0A%23%20Add%20trailing%20slash%20to%20*%2Fwp-admin%20requests.%0A%23%23%23%23%23%23%23%23%23%23%23%23%0A%0A%20%20%20%20%20%20%20%20rewrite%20%2Fwp-admin%24%20%24scheme%3A%2F%2F%24host%24uri%2F%20permanent%3B%0A%0A%0A%23%23%23%23%23%23%23%23%23%23%23%23%0A%23%20this%20prevents%20hidden%20files%20(beginning%20with%20a%20period)%20from%20being%20served%0A%23%23%23%23%23%23%23%23%23%23%23%23%0A%0Alocation%20~%20%2F%5C.%20%7B%0A%20%20%20%20%20%20%20%20access_log%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20off%3B%0A%20%20%20%20%20%20%20%20log_not_found%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20off%3B%0A%20%20%20%20%20%20%20%20deny%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20all%3B%0A%7D%0A%0A%23%23%23%23%23%23%23%23%23%23%23%0A%23%20SEND%20EXPIRES%20HEADERS%20AND%20TURN%20OFF%20404%20LOGGING%0A%23%23%23%23%23%23%23%23%23%23%23%0A%0A%20%20%20%20%20%20%20%20location%20~*%20%5E.%2B.(xml%7Cogg%7Cogv%7Csvg%7Csvgz%7Ceot%7Cotf%7Cwoff%7Cmp4%7Cttf%7Ccss%7Crss%7Catom%7Cjs%7Cjpg%7Cjpeg%7Cgif%7Cpng%7Cico%7Czip%7Ctgz%7Cgz%7Crar%7Cbz2%7Cdoc%7Cxls%7Cexe%7Cppt%7Ctar%7Cmid%7Cmidi%7Cwav%7Cbmp%7Crtf)%24%20%7B%0A%20%20%20%20%20%20%20%20access_log%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20off%3B%0A%20%20%20%20%20%20%20%20log_not_found%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20off%3B%0A%20%20%20%20%20%20%20%20expires%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20max%3B%0A%7D%0A%0A%23%23%23%23%23%23%23%23%23%23%23%23%0A%23%20Pass%20uploaded%20files%20to%20wp-includes%2Fms-files.php.%0A%23%23%23%23%23%23%23%23%23%23%23%23%0A%0A%23%20%20%20%20%20%20%20rewrite%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2Ffiles%2F%24%20%2Findex.php%20last%3B%0A%0Aif%20(%24uri%20!~%20wp-content%2Fplugins)%20%7B%0A%20%20%20%20%20%20%20%20rewrite%20%2Ffiles%2F(.%2B)%24%20%2Fwp-includes%2Fms-files.php%3Ffile%3D%241%20last%3B%0A%7D%0A%0A%23%20Rewrite%20multisite%20in%20a%20subdirectory%20’…%2Fwp-.*’%20and%20’…%2F*.php’.%0A%23%20if%20(!-e%20%24request_filename)%20%7B%0A%23%20%20%20%20rewrite%20%5E%2F%5B_0-9a-zA-Z-%5D%2B(%2Fwp-.*)%20%241%20last%3B%0A%23%20%20%20%20rewrite%20%5E%2F%5B_0-9a-zA-Z-%5D%2B.*(%2Fwp-admin%2F.*%5C.php)%24%20%241%20last%3B%0A%23%20%20%20%20rewrite%20%5E%2F%5B_0-9a-zA-Z-%5D%2B(%2F.*%5C.php)%24%20%241%20last%3B%0A%23%7D%0A%0A%23%20Rewrite%20multisite%20’…%2Fwp-.*’%20and%20’…%2F*.php’.%0Aif%20(!-e%20%24request_filename)%20%7B%0A%20%20%20%20rewrite%20%2Fwp-admin%24%20%24scheme%3A%2F%2F%24host%24uri%2F%20permanent%3B%0A%20%20%20%20rewrite%20%5E%2F%5B_0-9a-zA-Z-%5D%2B(%2Fwp-.*)%20%2Fwp%241%20last%3B%0A%20%20%20%20rewrite%20%5E%2F%5B_0-9a-zA-Z-%5D%2B(%2F.*%5C.php)%24%20%2Fwp%241%20last%3B%0A%7D%0A%0A%0A%23%23%23%23%23%23%23%23%23%23%23%23%0A%23%20Pass%20all%20.php%20files%20onto%20a%20php-fpm%20or%20php-cgi%20server%0A%23%23%23%23%23%23%23%23%23%23%23%23%0A%0Alocation%20~%20%5C.php%24%20%7B%0A%0A%20%20%20%20%20%20%20%20%23%20Try%20the%20files%20specified%20in%20order.%20In%20our%20case%2C%20try%20the%20requested%20URI%20and%20if%0A%20%20%20%20%20%20%20%20%23%20that%20fails%2C%20try%20(successfully)%20to%20pass%20a%20404%20error.%0A%20%20%20%20%20%20%20%20%23%20zero%20day%20exploit%20defense%0A%0A%20%20%20%20%20%20%20%20try_files%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%24uri%20%3D404%3B%0A%0A%20%20%20%20%20%20%20%20%23%20Include%20the%20fastcgi_params%20defaults%20provided%20by%20nginx%0A%0A%20%20%20%20%20%20%20%20include%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2Fetc%2Fnginx%2Ffastcgi_params%3B%0A%0A%20%20%20%20%20%20%20%20%23%20The%20amount%20of%20time%20for%20upstream%20to%20wait%20for%20a%20fastcgi%20process%20to%20send%20data.%0A%20%20%20%20%20%20%20%20%23%20We%20keep%20this%20*extremely*%20high%20so%20that%20one%20can%20be%20lazy%20when%20remote%20debugging.%0A%0A%20%20%20%20%20%20%20%20fastcgi_read_timeout%20%20%20%20%20%20%20%20%20%20%20%203600s%3B%0A%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%23%20Buffer%20size%20for%20reading%20the%20header%20of%20the%20backend%20FastCGI%20process.%0A%20%20%20%20%20%20%20%20%23%20This%20defaults%20to%20the%20value%20of%20a%20single%20fastcgi_buffers%2C%20so%20does%20not%0A%20%20%20%20%20%20%20%20%23%20need%20to%20be%20specified%20in%20our%20case%2C%20but%20it’s%20good%20to%20be%20explicit.%0A%0A%20%20%20%20%20%20%20%20fastcgi_buffer_size%20%20%20%20%20%20%20%20%20%20%20%20%20128k%3B%0A%0A%20%20%20%20%20%20%20%20%23%20The%20number%20and%20size%20of%20the%20buffers%20into%20which%20the%20reply%20from%20the%20FastCGI%0A%20%20%20%20%20%20%20%20%23%20process%20in%20the%20backend%20is%20read.%0A%20%20%20%20%20%20%20%20%23%0A%20%20%20%20%20%20%20%20%23%204%20buffers%20at%20128k%20means%20that%20any%20reply%20by%20FastCGI%20greater%20than%20512k%20goes%0A%20%20%20%20%20%20%20%20%23%20to%20disk%20and%20replies%20under%20512k%20are%20handled%20directly%20in%20memory.%0A%0A%20%20%20%20%20%20%20%20fastcgi_buffers%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%204%20128k%3B%0A%0A%20%20%20%20%20%20%20%20%23%20SCRIPT_FILENAME%20is%20a%20required%20parameter%20for%20things%20to%20work%20properly%2C%0A%20%20%20%20%20%20%20%20%23%20but%20was%20missing%20in%20the%20default%20fastcgi_params%20on%20upgrade%20to%20nginx%201.4.%0A%20%20%20%20%20%20%20%20%23%20We%20define%20it%20here%20to%20be%20sure%20that%20it%20exists.%0A%0A%20%20%20%20%20%20%20%20fastcgi_param%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20SCRIPT_FILENAME%20%24document_root%24fastcgi_script_name%3B%0A%0A%0A%20%23%20Use%20the%20upstream%20for%20php7.0-fpm%20that%20we%20defined%20in%20nginx.conf%0A%0A%20%20%20%20%20%20%20%20%23fastcgi_pass%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20unix%3A%2Frun%2Fphp%2Fphp7.2-fpm.sock%3B%0A%20%20%20%20%20%20%20%20fastcgi_pass%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20127.0.0.1%3A9000%3B%0A%0A%20%20%20%20%20%20%20%20%23%20And%20get%20to%20serving%20the%20file!%0A%0A%20%20%20%20%20%20%20%20fastcgi_index%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20index.php%3B%0A%7D%0A%0A%0A%23%23%23%23%23%23%23%23%23%23%23%23%0A%23%20ROBOTS%0A%23%23%23%23%23%23%23%23%23%23%23%0A%0A%20%20%20%20%20%20%20%20%20location%20%3D%20%2Frobots.txt%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20allow%20all%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20log_not_found%20off%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20access_log%20off%3B%0A%20%20%20%20%20%20%20%20%7D%0A%0A%0A%23%23%23%23%23%23%23%23%23%23%23%23%0A%23%20RESTRICTIONS%0A%23%23%23%23%23%23%23%23%23%23%23%23%0A%0A%23%20Deny%20access%20to%20any%20files%20with%20a%20.php%20extension%20in%20the%20uploads%20directory%0A%23%20Works%20in%20sub-directory%20installs%20and%20also%20in%20multisite%20network%0A%23%20Keep%20logging%20the%20requests%20to%20parse%20later%20(or%20to%20pass%20to%20firewall%20utilities%20such%20as%20fail2ban)%0Alocation%20~*%20%2F(%3F%3Auploads%7Cfiles)%2F.*%5C.php%24%20%7B%0A%20deny%20all%3B%0A%7D%0A%0A%7D%0A%0A” message=”” highlight=”” provider=”manual”/]

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:

[pastacode lang=”bash” manual=”Saving%20debug%20log%20to%20%2Fvar%2Flog%2Fletsencrypt%2Fletsencrypt.log%0APlugins%20selected%3A%20Authenticator%20standalone%2C%20Installer%20None%0AObtaining%20a%20new%20certificate%0APerforming%20the%20following%20challenges%3A%0Ahttp-01%20challenge%20for%20wp.computingforgeeks.com%0AWaiting%20for%20verification…%0ACleaning%20up%20challenges%0A%0AIMPORTANT%20NOTES%3A%0A%20-%20Congratulations!%20Your%20certificate%20and%20chain%20have%20been%20saved%20at%3A%0A%20%20%20%2Fetc%2Fletsencrypt%2Flive%2Fwp.computingforgeeks.com%2Ffullchain.pem%0A%20%20%20Your%20key%20file%20has%20been%20saved%20at%3A%0A%20%20%20%2Fetc%2Fletsencrypt%2Flive%2Fwp.computingforgeeks.com%2Fprivkey.pem%0A%20%20%20Your%20cert%20will%20expire%20on%202018-09-17.%20To%20obtain%20a%20new%20or%20tweaked%0A%20%20%20version%20of%20this%20certificate%20in%20the%20future%2C%20simply%20run%20certbot-auto%0A%20%20%20again.%20To%20non-interactively%20renew%20*all*%20of%20your%20certificates%2C%20run%0A%20%20%20%22certbot-auto%20renew%22%0A%20-%20If%20you%20like%20Certbot%2C%20please%20consider%20supporting%20our%20work%20by%3A%0A%0A%20%20%20Donating%20to%20ISRG%20%2F%20Let’s%20Encrypt%3A%20%20%20https%3A%2F%2Fletsencrypt.org%2Fdonate%0A%20%20%20Donating%20to%20EFF%3A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20https%3A%2F%2Feff.org%2Fdonate-le%0A” message=”” highlight=”” provider=”manual”/]

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 //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.

You can support us by downloading this article as PDF from the Link below. Download the guide as PDF