November 12, 2006

Posted by John

Tagged multisite

Older: Action Based Layouts

Newer: Class and Instance Variables In Ruby

Building A Multi-Site Application

Subdomains as account keys and multi-site applications are all the rage. All the cool kids are doing it and you want to as well but you just haven’t tried to figure it out yet. Well, if that is you then this is the article to read. Multi-site support is really easy with Rails and this article will go through the basics of getting a development environment setup on your mac (sorry windows users).

Creating the Rails App

Let’s create a new rails app ($ rails multisite), change to the new directory ($ cd multisite) and freeze it to edge rails ($ rake rails:freeze:edge).

Next we need to create the databases for development and test:

$ mysqladmin -u root create multisite_development
$ mysqladmin -u root create multisite_test

Now we’ll install a handy plugin created by DHH for working with subdomains and such:

$ script/plugin install http://dev.rubyonrails.org/svn/rails/plugins/account_location/

In order to actually show how this works, we need to have something to work with in the database so we’ll generate a new resource (which will create some scaffolding, a model and a migration).

$ script/generate scaffold_resource site name:string subdomain:string created_at:datetime updated_at:datetime

The above piece of code generated a migration for us, so let’s migrate to the newest version.

$ rake db:migrate

You should see output similar to below:

== CreateSites: migrating =====================================================
-- create_table(:sites)
   -> 0.1107s
== CreateSites: migrated (0.1115s) ============================================

Now it’s time to start up our app…

$ script/server

and open up your favorite browser (hopefully Firefox) to http://localhost:3000/sites/ so that we can enter a few test sites.

Click the ‘New site’ link and enter ‘Notre Dame’ for the name and ‘notredame’ for the subdomain. Press the ‘Create’ button, then click on the ‘back’ link, followed by the ‘New site’ link again. This time enter ‘Ohio State’ for the name and ‘ohiostate’ for the subdomain. Hit the ‘Create’ button again. You should now have a list view similar to the one below.

Site Listing

Setting Up Your Mac

Open up the application named NetInfo Manager (it is located in Applications/Utilities). Click the lock to make changes and enter your password.

Lock

Now click on machines and then on localhost.

Machines > Localhost

With localhost still selected, click duplicate and confirm the dialog box.

Duplicate

You should now have a machine named localhost copy. Rename this ‘notredame.multisite.local’ by double clicking on the name value.

Rename Duplicate

Now click back to localhost, click save and then click update copy (as the dialog’s appear). Repeat this step to create a machine named ‘ohiostate.multisite.local’. You now have two fake sites that you can tinker on your app with. To test it out, point your browser to http://notredame.multisite.local:3000/sites. You should get the same result as http://localhost:3000/sites.

But Which Site Am I On?

Now that we have the test sites created in the database and spoofed on our local machine, it’s time to add in the multi-site functionality. Because all controllers inherit from ApplicationController, all we need to do is add a before filter to it that figures out what site we are looking at and stores it. Open up app/controllers/application.rb and add in the following lines of code.

class ApplicationController < ActionController::Base
  include AccountLocation
  
  before_filter :find_current_site
  helper_method :current_site
  attr_reader   :current_site
  
  private
    def find_current_site
      @current_site = Site.find_by_subdomain(account_subdomain)
    end
end

So what does all that do? The first line includes the Account Location plugin methods in application controller. Next, I set a before filter to find the current site. After that, I added current_site as a helper method so that it can be used in the view (if necessary). The last line before the private declaration creates a read method for the instance variable current_site so that we can access the current site in all of our controllers.

The final piece of finding the current site is the find_current_site method. This attempts to find the current site based on the subdomain and set current_site equal to it. account_subdomain is a method from the account location plugin that we installed a bit ago.

Lastly, you could add the following line in your index.rhtml file to make sure that the current site is getting set properly.

<p><%= current_site.inspect %></p>

Now visit http://notredame.multisite.local:3000/sites/ and http://ohiostate.multisite.local:3000/sites/. Notice how the output of the inspection of current site differs. Below is a screenshot of when I visit the Notre Dame local site.

Current Site

That is it. Not nearly as difficult as you thought, eh? In case you didn’t follow along, you can download an archive of the app I just created (Edge Rails at the time of this post is also included). Sometime this week I’m going to write part two to this that shows how to write functional tests for multi-site apps.

14 Comments

  1. Daniel Lucraft Daniel Lucraft

    Nov 13, 2006

    I’m bookmarking this for when I need it right now. Out of interest, is it possible to run multiple domains (as opposed to subdomains) off one rails app. Can I point www.account1.com and www.account2.com to the same app?

  2. Daniel – if all you need is two domains pointed to the identical app, it’s easy – just set up both sites to point the same folder on your server. If you want to make the app behave differently based on the domain name, it’s basically the same thing – except you’ll need to apply similar techniques as this article (some routing inside your app).

    John – it’s worth noting that deploying the app means you need to set up wildcard DNS so that *.yourapp.com points to the right place. Shared hosts don’t always let you do this, either – Dreamhost says they generally won’t do it. “DreamHost may, in special cases, set this up for a customer.” As an alternative, you could write a script to create the proper DNS entry when the new site is created. Either way, your app won’t do it all alone.

  3. @Daniel – Yep, just change subdomain to domain and use find_by_domain(account_host) or something like that.

    @Chas – Nope this won’t do it all. This article is for setting up your local computer, not a remote server.

  4. Thanks for posting this, it was absolutely perfect. I had to restart my server since I absent-mindedly left it running.

  5. @Garrett – No problem. Glad you found it helpful. There’s more to come. :)

  6. Mark Meves Mark Meves

    Nov 14, 2006

    I found your walkthrough helpful although you missed a step: after “Now click on machines and then on localhost.” and before “You should now have a machine named localhost copy.”, add that you should click on the “duplicate” button.

  7. Just implemented your code and it works wonderfully. Thanks! Have a few extra tips to share.
    For domains instead of subdomains use find_by_domain(account_domain). Just remember that is you are running your server on port 3000, your domain entry in your database will have to be domain.com:3000.
    Also if you are on a Windows box you will have to edit your hosts file instead of the OSX NetInfo Manager. You can find it at c:\windows\system32\drivers\etc\hosts.

  8. @Eric – I didn’t have to add the port to the database on the actual project I am working on, however, I may have modified the account location plugin. The project that I pulled this article from works by the full domain, I just figured people would be more interested in a subdomain article.

  9. Now we want to buy machines for the industry of scaffolds: (PROPS, Pipe, Cup lock, Unvrsail-Jack ) Therefore, we hope that you help us to get the Pipes for the scaffolds industry: (PROPS, Cup lock, Unvrsail-Jack)

  10. Great solution! One question, wouldn’t it be better to put the data that currently exists in your sites table in a hash so we don’t have to make a trip to the database on every request?

  11. @Tony – I don’t think it would be, at least not in the app that I extracted this article from. If it was in a hash, each time I added a new site I would have to redeploy the entire application. With the sites stored in a database, I can modify sites and their configurations without redeploying and restarting mongrel. The database allows for more flexibility in this area.

    Also, most apps will have more information pertaining to sites that just a name and domain. Managing that information in a hash would quickly become unmanageable.

    Finally, by having sites stored in the database, I can use the rich domain modeling of rails.

  12. Tony Spencer Tony Spencer

    Mar 14, 2007

    Gotcha. To minimize the number of DB calls I’m putting the current site in the session.

    def find_current_site
    if !session[:currentsite].nil?
    @current_site = session[:currentsite]
    else
    @current_site = Site.find_by_domain(account_domain)
    session[:currentsite] = @current_site
    end
    end

    I wish there was a way to pull the current site into memory when the web server starts so I don’t have to make but one trip to the DB but I guess this solution is ok.

  13. I’m not a big fan of storing entire objects in the session.

  14. Hi there,

    I’m hosting an application (in production mode) with mongrel and apache (mongrel_cluster on ports 8000 to 8001) on a ubuntu feisty fawn server.

    Can someone tell me how I can do the following (or tell me where I can a tutorial to do it):

    - To have 2 differents domain names (dom1.com and dom2.org for an example) pointing the same rails app (but I would like that my rails app can know what domain was used : it’s for a multi-language site) What to do in apache ? in mongrel ? in rails ?

    - To use my server to host more than 1 ruby app and for different users (rails_app1 in /home/user1/public_html/rails_app1 ; rails_app2 in /home/user2/public_html/rails_app2 ; … and so on)

    For the moment I have mongrel (launched as user user1) and apache for hosting rails_app1 in /home/user1/public_html/rails_app1 (I made my set-up following http://mongrel.rubyforge.org/index.html and http://blog-perso.onzeweb.info/2006/12/16/rails-mongrel-apache-ubuntu-production/#comment-2135)

    I’ve read this site who explained how to manage subdomain. I didn’t understood everything (I’m not english native) but I don’t think this is what I really need.

    Thanks for help

Sorry, comments are closed for this article to ease the burden of pruning spam.

About

Authored by John Nunemaker (Noo-neh-maker), a programmer who has fallen deeply in love with Ruby. Learn More.

Projects

Flipper
Release your software more often with fewer problems.
Flip your features.