Vanilla forums on Nginx

This is an old post. It may contain broken links and outdated information.

A few years ago I created a web site called the Chronicles of George, featuring some badly-written help desk tickets from the job I had at the time. It gained me some small amount of Internet fame (but no fortune), and developed a loyal community of sympathizers. For a long time we hung out on a self-hosted phpbb forum, but a change in web hosting led to the opportunity to also change the forum software away from something as hack-prone and complex as phpbb to something faster, simpler, and ostensibly more secure: Vanilla.

Out of the box, Vanilla operates a bit differently from a traditional thread-based forum like phpbb. It is a discussion-focused forum, deprioritizing standard categorical organization in favor of bringing the things being talked about to the forefront. This has advantages in some forum models, like a support forum for a specific product or service, where the first thing a reader wants to see is discussion, not a choice of categories, but it’s not necessarily what most folks are used to seeing out of a web forum. Fortunately, Vanilla also offers configuration options to make it behave more like a “standard” web forum.

Why choose it, then, if we’re just going to override its most distinguishing characteristic? Because, as mentioned in the opening paragraph, it’s light and fast and secure. Additionally, the 2.1 branch (currently under development and downloadable here) comes with an absolutely killer theme that we can easily customize and prettify with some quick CSS.

Prerequisites

As with the other two major web applications about which I’ve blogged, MediaWiki and Wordpress, you’ll need some manner of PHP interpreter installed; for Nginx, the obvious choice is PHP5-FPMAlternative PHP Cache (APC) is also a good idea, along with memcached and PHP5-memcache for caching PHP sessiosn in RAM.

You’ll also need a database. Since PostgreSQL isn’t fully supported with Vanilla, I recommend the latest stable release of MariaDB, a drop-in replacement for MySQL without any of the ties to Oracle. There’s a MariaDB PPA available if you’re on Ubuntu and want to automatically keep things up to date.

Lastly, you need the Vanilla forum software. The latest stable version can be gotten from their download page, but it lacks a considerable number of desirable features and layout options. I recommend cloning their GitHub repository and running the forum from a copy of that, until such time as the 2.1 branch goes release.

Nginx Configuration

If you’re going to clone the Git repo, don’t run the forum directly from it, as it will make updates difficult. Instead, copy the clone’s contents (minus the actual Git folders) to your intended web root. We’re going to be running the forum on its own subdomain, rather than out of a subdirectory, as this will make the configuration a bit easier. It also means that pulls from the repo won’t clobber the existing site and lead to errors.

Below is the entire configuration in use for the forum, for cutting and pasting. We’ll go through it piece by piece. There is also a Vanilla configuration file to modify, which we’ll do after we get Nginx all set.

server {
    server_name your.forum.com;
    root /var/www-vanilla;
    index index.php default.php index.html;
    autoindex off;

# Root location
    location / {
        try_files $uri $uri/ @forum;
    }

# Rewrite to prettify the URL and hide the ugly PHP stuff
    location @forum {
# Start with this commented out until you configure it in Vanilla!
#		rewrite ^/(.+)$ /index.php?p=$1 last;
    }

# PHP handler
    location ~ \.php {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_pass php5-fpm-sock;
        fastcgi_split_path_info ^(.+\.php)(.*)$;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_intercept_errors on;
    }

# Stop things from executing in the uploads directory
    location ~* ^/uploads/.*.(html|htm|shtml|php)$ {
        types { }
        default_type text/plain;
    }

# Keep nosey people from discivering categories by number
    location ~* /categories/([0-9]|[1-9][0-9]|[1-9][0-9][0-9])$ {
        return 404;
    }

# Deny, drop, or internal locations
    location ~ /\. { access_log off; log_not_found off; deny all; }
    location ~ ~$ { access_log off; log_not_found off; deny all; }
    location = /robots.txt { access_log off; log_not_found off; }
    location ^~ favicon { access_log off; log_not_found off; }
    location ^~ /conf/ { internal; }

# Taking advantage of browser caching for static stuff
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|eot|woff|ttf|svg)$ {
        expires max;
        log_not_found off;
    }

# Change Vanilla's undesirable behavior & force it to display things like a traditional forum
    location = /discussions {
        rewrite ^ $scheme://your.forum.com/ permanent;
    }
    location = /discussions/ {
        rewrite ^ $scheme://your.forum.com/ permanent;
    }
    location = /categories/discussions {
        rewrite ^ $scheme://your.forum.com/ permanent;
    }
    location = /categories/discussions/ {
        rewrite ^ $scheme://your.forum.com/ permanent;
    }
    location /vanilla/discussions {
        rewrite ^ $scheme://your.forum.com/ permanent;
    }
    location /vanilla/discussions/ {
        rewrite ^ $scheme://your.forum.com/ permanent;
    }

# Plug-in specific rules
#
# Keep the WhosOnline plugin from flooding the logs
    location /plugin/imonline { access_log off; log_not_found off; }

}

Server directive basics

    server_name your.forum.com;
    root /var/www-vanilla;
    index index.php default.php index.html;
    autoindex off;

We open with the usual Nginx server directive, specifing the site’s hostname, the directory on disk where the site’s web root is located, and the files to use as index files for each subdirectory. We also disable autoindex generation, to keep people from poking around in your subdirectories. We’re omitting the listening port, which by default is port 80; if you were going to use a non-standard TCP port, this would be the place to put it.

Error handling

In the original version of the post, I had a section in here on overriding Vanilla’s default error handling some Nginx error_page directives, but it turns out that this causes some problems with displaying properly-skinned error pages; the default error page at dashboard/home/filenotfound isn’t a whole page, but rather gets included inside of another page when Vanilla serves it, and so calling it directly either yields another 404 error (which gets displayed as a standard Nginx 404) or an incomplete, malformed PHP response (which shows up as a blank page). The best way to serve up proper error pages is to rely on Vanilla’s built-in error handling, which doesn’t require you to do anything special at all.

Still, if you want to supply your own errors instead of using the built-in ones, you would do it with a block of code like this:

# Error handling
    error_page 404 /path/to/your/error/page.html;
    error_page 403 =404 /path/to/your/error/page.html;

I like to violate standards and return 404 responses to forbidden locations. A “403 Forbidden” feels too much like a big fat challenge that says, “There’s something interesting here but you can’t look at it!” Rather than wave a red flag over areas of the site that shouldn’t be accessible, it’s a lot less provocative to return 404 errors instead.

There are lots of other things you can do with custom responses within the Vanilla forum software, using routes, which function a bit like Nginx location directives. For more info on constructing custom routes, check out this page on the official Vanilla forum site.

Root location and main rewrite rule

# Root location
    location / {
        try_files $uri $uri/ @forum;
    }

# Rewrite to prettify the URL and hide the ugly PHP stuff
# Start with this commented out until you configure it in Vanilla!               
    location @forum {
#		rewrite ^/(.+)$ /index.php?p=$1 last;
    }

Here we set up our root location, using the try_files directive to tell Nginx that for any URI it receives, it should first see if the URI corresponds exactly to an object on the server, and then a directory, and then to pass the URI on to the forum named location.

Then, we define that named location. The reason for using a named location like this is so that we have something onto which we can tag our main SEO-friendly rewrite rule, which takes all the Vanilla forum URLs ending in index.php and makes them more human-readable.

However, the actual rewrite directive should be commented out until you’ve enabled friendly URLs in the Vanilla config file. Otherwise, your forum won’t work. We’ll go over that change after we’ve finished with the Nginx config breakdown.

PHP handler

    location ~ \.php {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_pass php5-fpm-sock;
        fastcgi_split_path_info ^(.+\.php)(.*)$;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_intercept_errors on;
    }

This is a standard PHP block to ensure that all PHP files are sent via Unix socket (pre-defined in another file with the alias php5-fpm-sock) to the PHP-FPM handler. The one difference between this PHP block and the others I’ve used before is the inclusion of the split_path_info (described here). This additional prameter helps PHP understand what part of the path being passed to it is the script it needs to execute and what parts are the arguments on which the script needs to act.

Security and obfuscation

# Stop things from executing in the uploads directory
    location ~* ^/uploads/.*.(html|htm|shtml|php)$ {
        types { }
        default_type text/plain;
    }

# Keep nosey people from discivering categories by number
    location ~* /categories/([0-9]|[1-9][0-9]|[1-9][0-9][0-9])$ {
        return 404;
    }

Like most forum solutions, Vanilla has a directory (uploads) where forum users can store avatars. Vanilla has restrictions in place to keep users from uploading files other than images, but it’s always possible that someone with mischief or mayhem on the brain will be able to work around those restrictions. As an added layer of security, the first location directive above forces potentially-executable content (HTML files, SHTML files, and PHP files) to be served out of the uploads directory with their mime types set as text/plain.

The second block works around a non-critical but potentially annoying issue. Categories (individual discussion forums, in traditional forum parlance) in Vanilla are browsable by name or by number. Having them browsable by name isn’t an issue, but being browsable by number means that curious folks can start plugging in numbers and get an idea of how many categories your forum employs. In a forum with moderator- or administrator-only forums that are otherwise hidden from view, this gives curious users more insight than they should have about the structure of your forum. So, to prevent this, the location directive above uses a regex to watch out for URLs containing /categories/1 through /categories/999 (along with variations with leading zeros) and serve up a 404 page instead.

Keeping your logs clean and adding a bit more security

    location ~ /\. { access_log off; log_not_found off; deny all; }
    location ~ ~$ { access_log off; log_not_found off; deny all; }
    location = /robots.txt { access_log off; log_not_found off; }
    location ^~ favicon { access_log off; log_not_found off; }
    location ^~ /conf/ { internal; }

This set of locations help tighten up some other potential security holes, as well as keep your logs from filling up with extraneous stuff. The first two block access to temp or hidden files (beginning with a dot or a dollar sign); the next two turn off logging for your site’s robots.txt file and favicon.

The last item throws up a barrier to accessing the /conf/ directory, where Vanilla keeps files that have potentially sensitive information in them (like the Vanilla MySQL account name and password). Now, on one hand, all of the config files are PHP files and are prefaced with this line:

if (!defined('APPLICATION')) exit();

This by itself is enough to keep them from being accessed as long as your PHP handler is functioning correctly. However, securing the files using the web server adds an extra layer of protection that will come into play if your web server’s PHP processes ever die.

In our case, we’re using Nginx’s internal directive, which allows the location to be accessed through a script or by the web server itself, but disallows serving files directly out of the location.

Caching

location ~*
    \.(js|css|png|jpg|jpeg|gif|ico|eot|woff|ttf|svg)$ {
        expires max;
        log_not_found off;
    }

This block instructs the web server to add a “max” expiry time to some common image and web font file types, so that those files will stay cached in users’ web browsers for as long as possible. This reduces the load on the web server by not having it dish up the same static, unchanging files over and over again.

Lots of rewrites

    location = /discussions {
        rewrite ^ $scheme://your.forum.com/ permanent;
    }
    location = /discussions/ {
        rewrite ^ $scheme://your.forum.com/ permanent;
    }
    location = /categories/discussions {
        rewrite ^ $scheme://your.forum.com/ permanent;
    }
    location = /categories/discussions/ {
        rewrite ^ $scheme://your.forum.com/ permanent;
    }
    location /vanilla/discussions {
        rewrite ^ $scheme://your.forum.com/ permanent;
    }
    location /vanilla/discussions/ {
        rewrite ^ $scheme://your.forum.com/ permanent;
    }

I prefer my forums to act like a traditional forum, without a flat view of all discussions. Vanilla provides some means to make that its default behavior, but some plug-ins break or bypass the default homepage view. It’s one thing for a user to manually access a list of all discussions, but I don’t ever want such a page served up automatically in response to a seemingly-innocent link. So, the above list of rewrites covers all of the scenarios I’ve found so far where this happens.

Setting these rules to be active is admin’s preference. If you like the discussion-oriented nature of Vanilla and prefer to minimize the role of categories, then you probably don’t want to use any of these.

Plugins

# Keep the WhosOnline plugin from flooding the logs
    location /plugin/imonline { access_log off; log_not_found off; }

We close with a location directive that prevents logging the pings from the WhosOnline plugin. This is an indispensable plugin which, as its name suggests, shows which users are currently on-line and browsing the forum. When active, this plugin’s user-generated responses clog up your log files like nobody’s business, so we’ll get rid of them.

Making the configuration live

All of the stuff above should go into a file in your /etc/nginx/sites-available directory. To actually bring the site online, symlink the file into your /etc/nginx/sites-enabled directory:

$ sudo ln -s /etc/nginx/sites-available/vanilla /etc/nginx/sites-enabled/vanilla

Then reload Nginx’s configuration ($ sudo /etc/init.d/nginx reload) and the configuration will be active. Once this has all been done, you can actually start the Vanilla forums install procedure.

Turning on URL rewrites

After you’ve gotten your forum up and running, you’ll probably want to enable pretty URLs. To do this, add the following line in conf/config.php:

$Configuration['Garden']['RewriteUrls'] = TRUE;

Then, uncomment the rewrite line in this section of your Nginx configuration:

location @forum {
    rewrite ^/(.+)$ /index.php?p=$1 last;
}

Finally, reload Nginx to make your changes active. Vanilla should now be up and running securely, with friendly URLs.

If you have any corrections or suggestions for improvements, please comment below!