WP multisite on nginx

There are a lot of articles out there on how to configure nginx to publish WordPress blogs in multisite mode. All of them are fatally incomplete. What follows is the exception.

That’s kind of harsh, I know.

Actually I did find one article that does a damn good job on this subject:

How to configure Nginx for WordPress Multisite with subdirectories

Maybe it’s that I wasn’t a fan of jigsaw puzzles as a kid, but I’ve always had a problem with most of what passes for technical documentation due to its incompleteness.

Take Nginx on the WordPress Codex, the main article on setting up nginx to work with WordPress. It’s not bad, really. I actually like the framework they put together. But, as acknowledged on the page itself, it is misleadingly incomplete when it comes to supporting multisite.

What’s missing are two critical pieces:

1. A definition for the $blogid variable; and
2. How to get it.

These two missing pieces are supplied in How to configure Nginx for WordPress Multisite with subdirectories, where the author provides an nginx map statement for multisite nginx virtual hosts (called “server blocks” in nginx-speak), and recommends the installation of the Nginx Helper plugin for getting the blog id values for each sub site on a multisite network.

Starting from scratch, here’s how I set things up on a CentOS 7 server.

1. Install nginx and all WordPress prerequisites (php, mariadb, mariadb-server, etc).

2. Create a database for the WordPress using the mysql client (after properly configuring the database server — at a minimum running mysql_secure_installation to set a root password and remove sample users and data).

3. Use this /etc/nginx/nginx.conf (basically the default nginx.conf that comes in the package with the addition of an include for “sites-enabled”):

# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*.conf;

    server {
        listen       80 default_server;
        listen       [::]:80 default_server;
        server_name  _;
        root         /var/www/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }
}

4. Create an /etc/nginx/conf.d/php-fpm.conf file with the following contents:

upstream php {
    server 127.0.0.1:9000;
}

5. Create a /etc/nginx/global/restrictions.conf file with these contents (this is verbatim from the WordPress Codex article):

# Global restrictions configuration file.
# Designed to be included in any server {} block.</p>
location = /favicon.ico {
	log_not_found off;
	access_log off;
}

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

# Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
# Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
location ~ /\. {
	deny all;
}

# 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;
}

6. Next make this /etc/nginx/global/wp-single.conf file (also verbatim from the Codex):

# WordPress single site rules.
# Designed to be included in any server {} block.

# This order might seem weird - this is attempted to match last if rules below fail.
# http://wiki.nginx.org/HttpCoreModule
location / {
	try_files $uri $uri/ /index.php?$args;
}

# Add trailing slash to */wp-admin requests.
rewrite /wp-admin$ $scheme://$host$uri/ permanent;

# Directives to send expires headers and turn off 404 error logging.
location ~* ^.+\.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|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;
}

# Uncomment one of the lines below for the appropriate caching plugin (if used).
#include global/wordpress-wp-super-cache.conf;
#include global/wordpress-w3-total-cache.conf;

# Pass all .php files onto a php-fpm/php-fcgi server.
location ~ [^/]\.php(/|$) {
	fastcgi_split_path_info ^(.+?\.php)(/.*)$;
	if (!-f $document_root$fastcgi_script_name) {
		return 404;
	}
	# This is a robust solution for path info security issue and works with "cgi.fix_pathinfo = 1" in /etc/php.ini (default)

	include fastcgi.conf;
	fastcgi_index index.php;
#	fastcgi_intercept_errors on;
	fastcgi_pass php;
}

7. Also make this /etc/nginx/global/wp-multi-sf.conf file (verbatim from the Codex):

# WordPress multisite subdirectory rules.
# Designed to be included in any server {} block.

# This order might seem weird - this is attempted to match last if rules below fail.
# http://wiki.nginx.org/HttpCoreModule
location / {
	try_files $uri $uri/ /index.php?$args;
}

# Directives to send expires headers and turn off 404 error logging.
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
	expires 24h;
	log_not_found off;
}

location ~ ^/[_0-9a-zA-Z-]+/files/(.*)$ {
        try_files /wp-content/blogs.dir/$blogid/files/$2 /wp-includes/ms-files.php?file=$2 ;
        access_log off; log_not_found off; expires max;
}

#avoid php readfile()
location ^~ /blogs.dir {
        internal;
        alias /var/www/example.com/htdocs/wp-content/blogs.dir ;
        access_log off; log_not_found off;      expires max;
}

# Uncomment one of the lines below for the appropriate caching plugin (if used).
#include global/wordpress-ms-subdir-wp-super-cache.conf;
#include global/wordpress-ms-subdir-w3-total-cache.conf;

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

# Pass all .php files onto a php-fpm/php-fcgi server.
location ~ \.php$ {
	# Zero-day exploit defense.
	# http://forum.nginx.org/read.php?2,88845,page=3
	# Won't work properly (404 error) if the file is not stored on this server, which is entirely possible with php-fpm/php-fcgi.
	# Comment the 'try_files' line out if you set up php-fpm/php-fcgi on another machine.  And then cross your fingers that you won't get hacked.
	try_files $uri =404;

	fastcgi_split_path_info ^(.+\.php)(/.+)$;
	#NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini

	include fastcgi_params;
	fastcgi_index index.php;
	fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
#	fastcgi_intercept_errors on;
	fastcgi_pass php;
}

8. Then make this /etc/nginx/conf/sites-available/site-blog.conf file named for your host (e.g. in this case, “blog.example.com”, the text is nearly identical to the Codex version with two important differences):

# Requires installation of Nginx Helper plugin for WordPress
# https://wordpress.org/plugins/nginx-helper

# map $http_host $blogid {
#    default 0;
#    include /var/www/html/blog/wp-content/uploads/nginx-helper/map.conf;
# }

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

	include global/restrictions.conf;
        include global/wp-single.conf;
        # include global/wp-multi-sf.conf;
}

9. Finally, make a soft link from /etc/nginx/sites-available/site-blog.conf to /etc/nginx/sites-enabled/site-blog.conf.

10. Do an nginx -t to make sure the configuration passes muster.

11. Now you can start (or restart) nginx and install WordPress. For the initial install be sure to make the nginx user owner of the directory where you install WordPress. I usually create a developer system user and group (e.g. “wpdev”) who I also give write access to the folder structure:

chown -R nginx:wpdev /var/www/html/blog
chmod -R g+w /var/www/html/blog

12. Note that by default WordPress sets up in single site mode. After you’ve test logged in for the first time be sure to update WordPress core if it’s out of date.

13. the switch to multisite mode is accomplished by adding the following line to wp-config.php above the line that reads “/* That’s all, stop editing!”:

/* Multisite */
define( 'WP_ALLOW_MULTISITE', true );

14. Once that’s done you need to log out and then in again and go to Tools… Network Setup.

The default multisite configuration should be for subdirectories (e.g. http://blog.example.com/site1, ../site2, etc.), but check to make sure before hitting the “Install” button.

Follow the instructions provided for further modifying wp-config.php and .htaccess (strictly speaking .htaccess isn’t used by nginx, but I modify it anyway).

15. You will be able to access the Network and default site dashboards even with the wp-single.conf configuration, so you can log out and log back in as instructed.

16. Creating and using additional sites will require activating site mapping and switching to the wp-multi-sf.conf configuration in the site’s server block.

17. As a prerequisite, install and configure the Nginx Helper plugin. Network activate it as requested. In its settings select “Enable Nginx Map” and then log out of WordPress.

18. Go to /etc/nginx/sites-available/site-[hostname].conf and change its configuration to fully accomodate multisite. To do that uncomment the map block above “server”, comment the include for wp-single.conf and uncomment that for wp-multi-sf.conf, thus:

# Requires installation of Nginx Helper plugin for WordPress
# https://wordpress.org/plugins/nginx-helper

map $http_host $blogid {
    default 0;
    include /var/www/html/blog/wp-content/uploads/nginx-helper/map.conf;
}

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

	include global/restrictions.conf;
        # include global/wp-single.conf;
        include global/wp-multi-sf.conf;
}

19. Once you’ve done this, run nginx -t to make sure the configuration passes and then restart nginx.

20. You should now be able to log into your multisite WordPress installation, as well as create and access multiple sites under it.

21. As a matter of housekeeping I also reset the permissions on the WordPress files thus:

chown -R wpdev:wpdev /var/www/html/blog
chmod -R g+w /var/www/html/blog
chown -R nginx:wpdev /var/www/html/blog/wp-config.php
chmod u-w /var/www/html/blog/wp-config.php
chown -R nginx:wpdev /var/www/html/blog/wp-content
This entry was posted in System Administration, Web on by .

About phil

My name is Phil Lembo. In my day job I’m an enterprise IT architect for a leading distribution and services company. The rest of my time I try to maintain a semi-normal family life in the suburbs of Raleigh, NC. E-mail me at philipATlembobrothersDOTcom. The opinions expressed here are entirely my own and not those of my employers, past, present or future (except where I quote others, who will need to accept responsibility for their own rants).