Hosting Multiple Wordpress Sites on a Single Server With NGINX

Table of Contents

Required Packages

You’ll need these packages installed on your Linux server to complete this tutorial.

  • php
  • php-mysql
  • mysql
  • nginx

Installing WordPress

Use wget to download WordPress. If you don’t have access to sudo on your server, you can download the file locally and use scp or a file manager to upload it to your server instead.

--2017-07-29 16:32:12--
Resolving (,
Connecting to (||:80... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: [following]
--2017-07-29 16:32:12--
Connecting to (||:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 8169865 (7.8M) [application/octet-stream]
Saving to: ‘latest.tar.gz’

latest.tar.gz                 100%[==============================================>]   7.79M  3.02MB/s    in 2.6s

2017-07-29 16:32:15 (3.02 MB/s) - ‘latest.tar.gz’ saved [8169865/8169865]

For each WordPress install, create a directory with your site’s domain name at /var/www. Create an html directory within your site’s domain directory.

# note: -p tells mkdir to create all parent directories
mkdir -p /var/www/

Now, unzip the latest.tar.gz file and copy the files to the /html directory.

tar -xf latest.tar.gz
# use -r for recursive copy
cp -r wordpress/* /var/www/

To ensure everything runs smoothly, change the owner of the directory to www-data (the -R changes ownership recursively).

chown www-data:www-data -R

Your WordPress install is now ready to be configured; let’s wire up the other components we’ll need.

Setting Up the Database

Connect to your mysql instance as root (or a user with admin privileges):

mysql -u root -p

Create the new database:

mysql> CREATE DATABASE site_1_db;

Create the user, then grant it all privileges to its respective database:

mysql> CREATE USER 'site_1_db_user'@'localhost' IDENTIFIED BY 'yourpassword';
mysql> GRANT ALL PRIVILEGES ON site_1_db.* TO 'site_1_db_user'@'localhost';
mysql> quit

That’s all we need to do with mysql for now; we’ll connect our site to its database once the nginx server configuration is completed.

Setting Up NGINX (Your Traffic Controller)

Nginx basic file architecture

  • /etc/init.d/nginx: Service controller
    • sudo /etc/init.d/nginx start starts the server
    • sudo /etc/init.d/nginx stop stops the server
    • sudo /etc/init.d/nginx restart restarts the server
  • /etc/nginx/nginx.conf: Base configuration
  • /etc/nginx/sites-available: Site configuration files (virtual hosts)
    • Files in this dir are symlinked to /etc/nginx/sites-enabled
    • When the nginx process is started, nginx.conf tells nginx to slurp up all the files in /etc/nginx/sites-enabled with this instruction: include /etc/nginx/sites-enabled/*;
  • /etc/nginx/conf.d: Additional configuration files
    • Similarly to sites-available, files in this dir will get loaded at nginx process runtime, with this instruction in the base configuration file: include /etc/nginx/conf.d/*.conf;
    • The minor difference is that only files with .conf endings will be loaded
    • We won’t be using these files in this configuration
  • /etc/nginx/mime.types: Mime type (identifiable file formats) configuration
    • These are loaded at nginx runtime by this instruction in the base configuration: include /etc/nginx/mime.types;

Install and Test NGINX

sudo apt-get install nginx

You could fire up the nginx process right away with the default server settings in /etc/nginx/sites-available/default, but all you would see is the nginx confirmation page. If you have apache running on the same port (default is 80), you will receive an error, so be sure to shut that down first with a sudo /etc/init.d/apachectl stop or just sudo apachectl stop.

Go ahead and fire up the server and navigate to localhost or in your web browser, or do a quick curl from the command line:

sudo /etc/init.d/nginx start
# Starting nginx (via systemctl): nginx.service.
curl localhost

You should be seeing the default welcome document which is stored at /var/www/html/index.nginx-debian.html.

How does nginx find this file?

If you look at the default server settings in /etc/nginx/sites-available/default, you might notice that the root setting is pointing to the default html directory: /var/www/html. You also might notice that index.nginx-debian.html is in the index list. This list gets loaded in order. That means if you add your own HTML doc index.html (you’ll likely need to be root to add a file to /var/www/html), that will load instead. Try it out!

echo 'Hello world!' >> /var/www/html/index.html
curl localhost
# Hello world!

Modifying the Default Site Settings

Let’s dig into the default settings file.

$ cat /etc/nginx/sites-enabled/default
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# Generally, you will want to move this file somewhere, and start with a clean
# file but keep this around for reference. Or just disable in sites-enabled.
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.

# Default server configuration
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        # SSL configuration
        # listen 443 ssl default_server;
        # listen [::]:443 ssl default_server;
        # Note: You should disable gzip for SSL traffic.
        # See:
        # Read up on ssl_ciphers to ensure a secure configuration.
        # See:
        # Self signed certs generated by the ssl-cert package
        # Don't use them in a production server!
        # include snippets/snakeoil.conf;

        root /var/www/html;

        # Add index.php to the list if you are using PHP
        index index.html index.htm index.nginx-debian.html;

        server_name _;

        location / {
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                try_files $uri $uri/ =404;

        # pass the PHP scripts to FastCGI server listening on
        #location ~ \.php$ {
        #       include snippets/fastcgi-php.conf;
        #       # With php7.0-cgi alone:
        #       fastcgi_pass;
        #       # With php7.0-fpm:
        #       fastcgi_pass unix:/run/php/php7.0-fpm.sock;

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

# Virtual Host configuration for
# You can move that to a different file under sites-available/ and symlink that
# to sites-enabled/ to enable it.
#server {
#       listen 80;
#       listen [::]:80;
#       server_name;
#       root /var/www/;
#       index index.html;
#       location / {
#               try_files $uri $uri/ =404;
#       }

What is all that stuff? Let’s prune it up a bit.

First, make a backup in case things go awry. This is always a good practice when changing any configuration file.

cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default.backup

Now we can get down to business removing the parts we don’t need (or (#default-nginx-config”>jump to the pruned-up version]).

Get rid of the comments at the top and the comments regarding SSL (we don’t need to worry about that for now).

Add index.php to the front of the index list as indicated by the comment, and delete the last two entries (don’t delete the semicolon!)

index index.php index.html;

Update the location / section to handle PHP:

# replace
try_files $uri $uri/ =404;
# with
try_files $uri $uri/ /index.php?q=$uri&$args;

Uncomment the location ~ \.php$ section. Delete the line that ends with :9000.

Delete the .htaccess; we won’t be using apache at all.

Delete the virtual hosts section at the bottom; we’ll get to that later.

Your default file should now look like this:

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

        root /var/www/html;

        index index.php index.html;

        server_name _;

        location / {
                try_files $uri $uri/ /index.php?q=$uri&$args;

        location ~ \.php$ {
                include snippets/fastcgi-php.conf;
                fastcgi_pass unix:/run/php/php7.0-fpm.sock;

That’s better, but it’s still pointing to the default /var/www/html, and we want it to load our WordPress installation.

Mapping Your WordPress Install to Your NGINX Server

First, let’s get rid of the default symlink /etc/nginx/sites-enabled/default. Don’t worry, the configuration file will remain untouched in /etc/nginx/sites-available.

rm /etc/nginx/sites-enabled/default

Next, make a copy of /etc/nginx/sites-available/default. You can name it whatever you want, but I recommend using your domain name.

cp /etc/nginx/sites-available/default /etc/nginx/sites-available/

Now, make a symlink to sites-enabled and restart your nginx server.

sudo ln -s /etc/nginx/sites-available/ /etc/nginx/sites-enabled/
sudo /etc/init.d/nginx restart
# or
service nginx restart

You should see the exact same 'Hello world!' page as before when you curl or visit localhost in your browser.

Now it’s time to wire up the WordPress directory. Simply change the root directory to /var/www/

vim /etc/nginx/sites-available/
# change 'root /var/www/html;' to 'root /var/www/;'
# restart nginx
sudo /etc/init.d/nginx restart

Now when you navigate to localhost with your browser you should see the WordPress setup page, prompting you to select a language.

The next page will prompt you for the database credentials you created earlier. The WordPress installer will test the connection, and then you can proceed with setting up your site.


Troubleshooting: PHP Processing Errors

If you see raw PHP code when you navigate to localhost, you’ll need to update your nginx MIME types.

vim /etc/nginx/mime.types

Insert the text/php type at the top of the list like so:

types {
    text/php                              php php5;
    text/html                             html htm shtml;

Troubleshooting: Possible Missing Packages

If you get a 404 or a 502, you may be missing some required packages. If you’re using a Digital Ocean droplet and you selected a WordPress install when you created the droplet, these are taken care of for you. If you’re rolling your own server setup, you’ll need to install php and the php-mysql extension.

$ sudo apt-get install php
$ sudo apt-get install php-mysql

Once you’re done with your WordPress setup, it’s time to inflict your site upon the world. To do that, you’ll need to setup your domain’s DNS records.

Setting Up DNS (Domain Name Server)

You’ll want to have (at least) 2 DNS records for each of your sites. Log in to your DNS manager and create one A record and one CNAME record for each site. The A record should point to the same public IP address (your server’s).

Click here for Nameserver instructions

Click here for DNS record instructions (they’re for setting up a droplet on DigitalOcean, but the principle is fairly standard.)

Once your records are configured properly to point to your host IP, you should be able to access your site via your domain name.

NGINX Server Matching and You: Server Lists and the Catchall Server

“But wait, we didn’t update the server list in the file. How does it load my WordPress files for both localhost and my domain name/public IP?”

Glad you asked! The matching magic occurs in the first lines of the file.

listen 80 default_server;

This server is matched by any incoming traffic on port 80. In fact, 'listen 80' is actually shorthand for 'listen *:80': any traffic coming in on port 80 (the default HTTP connection port). But how? What does server_name _; do?

First, nginx looks for all available servers. Then it tries to match the HOST_NAME given in the HTTP request to the servers list. In this case, it tries to match localhost, your domain name, or your public IP to _, which fails.

So why does it still load your site? Because there is only one server defined, and nginx is super generous. When it doesn’t find a match, nginx will serve up the first site it can find. So the last-loaded site is the site that gets served when no hosts are matched (unless a default_server is defined, then that will get loaded when no matches are found). That’s super helpful of nginx, isn’t it?

To protect against inappropriate or accidental serves, you’ll want to define a catchall server. Let’s take care of that before we get our second WordPress site rolling.

Configuring the catchall Virtual Host

Create a new configuration file in sites-available:

$ vim /etc/nginx/sites-available/catchall

Add these contents to catchall:

server {
        listen 80 default_server;
        root /var/www/html;
        index index.html;

        server_name "";

        location / {
                try_files $uri $uri/ /index.html;

Don’t forget to symlink the catchall server config to sites-enabled:

sudo ln -s /etc/nginx/sites-available/catchall /etc/nginx/sites-enabled/

That’s it (almost)! We’ve used a zero-length string as the server name, but something like the default _ would work just as well. Basically you want the file with the default_server setting to never match any HOST_NAME. When nginx can’t find a match in any of it’s known servers, it will load the server flagged as the default_server. In our case, it will load the index.html file at /var/www/html, so put something snarky there for anyone trying to direct traffic to your public IP. At least provide some entertainment since they would have gone to all the trouble.

If you restart your nginx server now, you will get an error:

sudo /etc/init.d/nginx restart
Restarting nginx (via systemctl): nginx.serviceJob for nginx.service failed because the control process exited with error code. See "systemctl status nginx.service" and "journalctl -xe" for details.

Take a look at the journalctl log to see what the problem is (spoiler alert: we have multiple default servers defined):

journalctl -ex
# aha! duplicate default server!
Jul 29 17:55:47 hostname nginx[5118]: nginx: [emerg] a duplicate default server for in /etc/nginx/sites-enabl


Open up your configuration file and remove default_server from the listen line:

# before
server {
        listen 80 default_server;
# after
server {
        listen 80;

While we’re at it, let’s add our domain name to the server_name list. You’ll want to add two items, one to match exactly, and another to match anything prefixed to the domain name, like the www CNAME record or a sub-domain:

# before
server_name _;
# after
server_name *;

That’s it!

Now you can start your nginx server and bask in the sweet, sweet glow of your default WordPress theme (mine has a very soothing image of a potted succulent).


Additional WordPress Installs

At this point, all of the pieces are in place. For additional WordPress installs, you’ll simply need to repeat the steps above for, and so on:

  • Set up the WordPress install at /var/www/
  • Set up a new database and database user (or you could use the same database with a different table prefix, but why tempt fate?)
  • Copy /etc/nginx/sites-available/ to /etc/nginx/sites-available/
    • Change the server_name list and the root list to match
  • Restart your nginx server
  • Set up your DNS for (Note: You don’t actually have to have a second domain; you could create a sub-domain on and simply point the second virtual server configuration’s server_name to that sub-domain! Just be sure to modify the * server_list entry first, of course.)
  • Navigate to your new WordPress install and go through the configuration steps.
  • (Required) Put two browser windows side-by-side, with one site in each, and bask in the glow of dual succulents!

A Parting Anecdote

Or “How a semicolon forced me to gain a deeper understanding of nginx than I probably needed”

I was having problems setting up multiple virtual hosts with nginx.

The site configuration was set up appropriately (I thought) for two separate domains in sites_enabled, but nginx was only serving up one or the other site.

By toggling default_server in first one file, then the other, I verified nginx was able to load both root locations.

To facilitate troubleshooting, I stripped the virtual server files down to barebones: no WordPress loading, just a simple index.html telling me which root location was loading. If I set site1 to default_server, that configuration’s root location would load for both domain names, and vice versa.

After pounding my head against this problem for hours, digging into nginx documentation, and reading tutorials on multiple-site setups, I still couldn’t find what was causing the problem.

It was clear that I was experiencing the nginx generosity that we discussed above: nothing was being matched, so it was serving up the default_site instead. I just couldn’t figure out why.

Finally, I started tweaking the order of definitions in the sites-available configuration, and restarting the nginx service with each tweak.

Lo! When I moved the server_name list above the index list, the nginx service failed to start, and journalctl had this to say for itself:

Jul 29 18:42:47 asus3 nginx[11671]: nginx: [emerg] directive "index" is not terminated by ";" in /etc/nginx/sites-enabled/

Aha! A missing semicolon at the end of the index list! I added the semicolon and all was right with the world.