Announcing Multisite Plugin for Rails

June 25, 2007     

A production release of this plugin is now available for Rails 2.0.2, no more core patch required! See the announcement  

Following up on my previous post about multiple controller view paths, I’m now releasing my multisite plugin for Rails. It’s not yet fully tested, but it’s already in production and will be fully tested for at least one use case by the end of the week. I wanted to get the code out ASAP to garner feedback, because I’m getting the feeling that there is a lot of community interest in this. However, please note the following:

  • This plugin requires edge Rails plus a patch that is still pending approval from core.
  • There is currently one unaddressed issue that may need to be resolved.

Multisite Plugin

What Rails multisite does is to insert an additional view path into the search path run by ActionView when it’s looking for a template. The basic use case is that you have multiple domains pointing to the same Rails application, and you want a given domain to be able to override any of the standard templates in app/views. However, the plugin doesn’t make any assumption about what constitutes a “site”. You are responsible for writing your own method to select a site.

Multisite is based on Matt McCray’s Theme Support plugin for its structure, but is essentially all new code taking advantage of edge Rails’ multiple controller view paths. I haven’t put in any support for Liquid templates or the static file theme caching. I’ve set things up a little differently.

Controller Code

I’m basing my site selection on the main chunk of the primary domain name, so I set it up something like this:

class ApplicationController < ActionController::Base
  site :get_site

  def get_site
    request.domain.split('.')[-2] #'www.darwinweb.net' returns 'darwinweb'

File Structure & Generator

The location of the customized templates will be sites/darwinweb/views (within your RAILS_ROOT). This views directory behaves just like the regular Rails app/views directory, and it is searched first. Primarily I use this for layouts, but any template can be put in there to override a default application template.

You can generate the very simple directory structure by running:

script/generate site darwinweb

This will generate the views directory as well as a second directory: sites/darwinweb/public. Now this public directory doesn’t do anything on its own. I opted to link this in to each site via Apache VirtualHost configs. I found this to be much cleaner than requiring link helpers such as theme_stylesheet_link that cache files into a subdirectory of the main public directory.

Apache VirtualHost Config

So if this new public directory becomes the Apache DOCUMENT_ROOT, does each site have to have a copy of all the shared files? Well, no, Mongrel will server static files from the main public directory by default if Apache passes the request through. However using Mongrel for static content isn’t recommended, so I came up with an additional rewrite rule to allow Apache to check both directories before proxying the request. This is based on the stock Railsmachine config:

<VirtualHost *:80>
  ServerName darwinweb.net

  DocumentRoot /var/www/apps/rails_app/current/sites/darwinweb/public

  # Configure mongrel_cluster
  <Proxy balancer://quickidx_cluster>

  RewriteEngine On
  # Check for maintenance file and redirect all requests
  RewriteCond %{DOCUMENT_ROOT}/system/maintenance.html -f
  RewriteCond %{SCRIPT_FILENAME} !maintenance.html
  RewriteRule ^.*$ /system/maintenance.html [L]

  # Rewrite index to check for static
  RewriteRule ^/$ /index.html [QSA]

  # Rewrite to check for Rails cached page
  RewriteRule ^([^.]+)$ $1.html [QSA]

  # Rewrite to check for main application static file.
  RewriteRule ^/(.*)$ /public%{REQUEST_URI} [QSA]

  # Redirect all non-static requests to cluster
  RewriteRule ^/(.*)$ balancer://quickidx_cluster%{REQUEST_URI} [P,QSA,L]

I’ve left a few of the esoteric parts out, but this config is essentially a copy of the base application config with a new ServerName and DocumentRoot. The tricky bit is the second to last RewriteRule which rewrites the URL to a subdirectory if the requested file doesn’t exist. In order for that to work, you need to have a symlink pointing the main application /public directory inside your site’s /public directory. I tried to get it working without a symlink, but mod_rewrite can’t change the DocumentRoot, so the only way to break out of sites/darwinweb/public is with a symlink. You’ll need to make sure Options +FollowSymLinks is enabled.

Development Plans

I haven’t released an official packaged version of this plugin yet because I haven’t had a chance to check out the issue with compiled template names that Rick Olson pointed out in my patch ticket. Currently I’m running only a single customized site, but that is about to change, so an official release is forthcoming.

More View Paths

Aside from the basic functionality on a site-by-site basis, I’ve also discovered that adding view_paths has a tremendous utility for selecting different templates without messing around with Rails’ implicit rendering. In a sense this returns to the original theming concept of Theme Support, but I’m looking at creating a finer-grained model. I don’t have a firm implementation plan for that, but it seems a natural fit for the plugin.

Liquid Templates

In one sense Liquid templates are an orthogonal solution to the same problem I’m trying to solve with Multisite. However Liquid provides the additional benefit of end-user-safe customization, which is definitely a killer feature. I haven’t had the time to set up Liquid templates on my current project, but they are definitely in the long-term plan. As such, they will need to be integrated into Multisite eventually.

Your Input

I’ve received positive feedback from several people already, so I know this plugin is scratching more than my own itch. But everyone’s use case is different, so I’m really reaching out for others’ input to make Multisite serve a wider audience. I’m generally interested in productizing high-end web apps, where clients are paying big bucks for deep customization capability. This is different from the 37Signals style of apps, but I believe there’s a huge market niche for this kind of stuff. If you’re developing a productized application in Rails, I’d love to hear from you.

Xin says…
June 26, 2007 at 8:09AM

This is great. I got very excited when I saw this post title in RubyCorner.

Rails not being able to run multiple sites has been irritating me too. It is simply too resource-intensive to have 2 mongrels per application, especially when some of them may receive sporadic traffic.

If I’m not mistaken, this plugin can help solve that. It does this by having all controllers bunched together as usual, but separates the views into sites?

Perhaps the controllers and models can be separated too.

Most Rails application obviously reside in their own directory structure. A Rake command that combines multipe apps into one with multisite-enabled would be very useful.

Again, I hope I’ve got the right end of the stick here.

Gabe da Silveira says…
June 26, 2007 at 8:25AM

Right, it just provides you a way to customize views per site. You can still use your base application views as defaults. In my case I’m mostly overriding layouts/application.rhtml.

As far as separating controllers and models per site, that would be a much deeper issue to tackle. It sounds like the issue you’re concerned about is memory usage, which I’ve written about before. I have to say that I still think Rails is a poor choice for sites that can’t afford the memory for dedicated mongrels. I suspect shoehorning separate applications into the same mongrel instance will raise some really intractable issues. That’s not to say that a dedicated effort couldn’t find a way, but it’s definitely outside the scope of this plugin for the foreseeable future.

Andre P. says…
June 30, 2007 at 10:57AM

Wow! I’ve been dreaming of sth like this for a long time… :)

I really hope the Rails Core team will accept your patch, Gabe!

What I don’t like about the strategy outlined in this article is the part about using Apache and a custom config for the individual ‘public’ dirs. — And, BTW, does this mean that to add a new site/domain we have to update this config file? I hope not! ’Cause that would prevent the new site setup process from being fully automated.

Gabe da Silveira says…
July 1, 2007 at 1:53AM

And, BTW, does this mean that to add a new site/domain we have to update this config file?

The short answer is “No.” The plugin is only concerned with providing access to the custom views, and that has nothing to do with your Apache config. You can set up access to your static files however you want.

Theme support provides a facility for this by caching (for example) stylesheets into public/themes/darwinweb/stylesheets. I did not carry this paradigm forward because it didn’t work well for me for several reasons:

  • No support for static .html files. It allows stylesheets, images, javascript and that’s it.
  • Can’t drop existing designs and sites in easily as all the links must start with /themes/darwinweb/*.
  • Adds mental and computational overhead of copying files around instead of Apache just referring to them directly.
  • Some of my team members don’t use SVN, but they need access to the static CSS and image files via FTP. The cached copy is a red herring since it will work to edit it directly, but it’s not the “official” copy and thus changes will be lost as soon as it is re-cached.

By comparison, direct Apache config makes it possible to take existing sites (even containing PHP!) and drop them into the public directory with zero modifications. The value of this can not be underestimated when you are selling a product to people who already have a website.

Also, my sites directory is not under version control. I debated this for a long time internally, and came to the conclusion that storing all those custom files (especially large amounts of static files) would bog down the repository needlessly. Plus, as a side benefit, the CSS and other static files can be edited via FTP by non-programmers very easily. I’m sure some of you are groaning at this point, but in practice this setup is the ideal for my situation. Eventually I’d like to get all my colleagues up to speed with subversion, but even so that doesn’t solve the problem of the repository size ballooning out of control as we add some very large sites with thousands of static files into the system.

I hope not! ‘Cause that would prevent the new site setup process from being fully automated.

Each VirtualHost has it’s own file, and they follow a standard template, so automating the Apache config part is actually quite straightforward. In my case adding a domain to the site already requires a new VirtualHost to be set up anyway, because we are running more than one app on the server, so each domain must have a VirtualHost entry.

That’s not to say this setup is ideal for everyone, but it does have some huge performance and maintenance benefits. If someone has some ideas to integrate directly into the plugin, my ears are open, but it’s gotta go beyond theme_support’s caching approach.

Ben says…
August 12, 2007 at 1:45AM

Gabe, thanks for releasing this as a plugin. I will be needing something like this soon. I agree with your previous post about Rails “not scaling down” and I think this a great way to approach this issue in some situations. In the past I have not only had to change the layouts based on URI but also the calls I make to the DB. I’m working on some code (maybe a plugin eventually) that will scope SQL calls based on the URI. (So models will have to have some sort of relation, whether direct or indirect, to a Site/Host and so all the finds will be scoped accordingly.) I’m just in the beginning stages but I have done something similar on a past site. Since you seem to be doing similar things I was wondering if maybe you have had the same issue of having to scope models based on URI and maybe you could give me some feedback on how to approach the problem. Thanks again.

Gabe da Silveira says…
August 12, 2007 at 2:04AM

Well in my app I have a variable called @site_user which is set in a before_filter based on the domain. The user model has a number of methods that return find options hashes or directly query the database for the data that is applicable to that user. I haven’t put much thought into any kind of global scoping, because my app is a real estate app, and a lot of the data actually is shared. I would be curious to look inside Basecamp and see how they do it…