[Home] You are not logged in. Login here

Just my stuff

Subdomain routing with rails

s2.diffuse.it, gpichler.diffuse.it, adanti.diffuse.it and so on. There are a few problems you have to overcome if you want your users to have their own subdomain:
1. first of all you need a dns wildcard
2. you need to configure apache
3. you need to differentiate between assets (javascripts, images, stylesheets) that are on the main site, and those from the subdomains
4. cookies
5. links to subdomains
The first problem is easy to solve: just tell your dns provider to route all traffic from *.yourdomain.tld to the same ip.
The second problem is easily solved too, with a catch all vhost in apache:

<VirtualHost *:80>
  ServerName yourdomain.tld
  DocumentRoot /opt/myapp/public/
  RequestHeader set SERVER_NAME yourdomain.tld
  ProxyRequests Off

  ProxyPass / http://127.0.0.1:8000/
  ProxyPassReverse / http://127.0.0.1:8000/
</VirtualHost>

If this is your first vhost in apache, then all requests going to the ip of your web server will end up in that vhost definition, this means that whatever.yourdomain.tld will end up using that vhost definition (I am assuming we have apache in front of a mongrel listening on 8000 here, but of course this will work with fcgi or mod_rails too).
Ok, now we have all requests to whatever subdomain proxyed to our mongrels on 8000. The RequestHeader set SERVER_NAME part is just a variable I added to the request. I will use this later in rails to get the current host name without any subdomain.
Now let’s move to the rails part. Groovie has very good support for subdomains with it’s routes, but rails (to my knowledge) does not, so I had to monkeypatch it a bit.
The first thing I did was to add a before_filter to get the current user by vhost:
class ApplicationController < ActionController::Base
  before_filter :get_appuser
  before_filter :get_vhostuser

  def get_appuser
    @appuser = User.find(session[:user_id])
  end

  def get_vhostuser
    @vhostuser = User.find_by_vhost(request.subdomains.first)
  end
end

This way we know what subdomain the current request is on, and who the logged in user is (if he is).
Next we have to make sure that static assets are served always from the root domain (yourdomain.tld). If we use
stylesheet_link_tag “style”
the generated url will be relative to the current host:
<link href=”/stylesheets/style.css” media=”screen” rel=”stylesheet” type=”text/css” />
but we want it to be always our root domain, so the browser can cache them and does not download the same stylesheets and javascripts each time for each subdomain. To do this we can just use
config.action_controller.asset_host = 'http://yourdomain.tld'

in environments/production.rb. This way all links pointing to static assets will be generated with http://yourdomain.tld in front of them.
Taken care of the static assets, we have to deal with the cookies. If a user logs in on http://yourdomain.tld/login, we want him to be logged in on http://username1.yourdomain.tld/ as well, and on http://username2.yourdomain.tld/ and so on. To archive this we have to set the domain of the session cookie to .yourdomain.tld.
Now we have almost everything in place. What we still need is some helpers to play nice with the subdomains. Wouldn’t it be nice if you could
<%= link_to 'Home', :controller => 'home', :action => 'index', :subdomain => 'user1' %>
?
Yes, it would. And here comes the ugly part. To do this I had to monkeypatch the UrlRewriter
#I took this from somewhere on the net,
#i don't remember who wrote it so I can't
#really give credit, but I did not write it!
module ActionController
  class UrlRewriter
    def rewrite(options = {})
      unless options[:subdomain].nil?
        if options[:subdomain] == false
          newhost = @request.server_name #domain(options[:tld_length] || 1)
        elsif @request.vhost != options[:subdomain]
          newhost = "#{options[:subdomain]}." + @request.server_name
        end
        unless newhost.nil?
          options[:host] = @request.port == @request.standard_port ? "#{newhost}" : "#{newhost}:#{@request.port}" 
          options[:only_path] = false
        end
      end
      options.delete(:subdomain)
      rewrite_url(options)
    end
  end
end

Just make a plugin out of this code, or put it in a file in the inizializers directory. But, where does that @request.server_name and @request.vhost come from I hear you all (the 2 users reading this post) scream? I patched the AbstractRequest too, to add this two methods:
module ActionController
  class AbstractRequest
     def server_name
       @env['HTTP_SERVER_NAME']
     end
     def vhost
       subdomains.first
     end
  end
end

Remember the
RequestHeader set SERVER_NAME yourdomain.tld
part in the apache config file? Here we use it to get the root domain name.
Well, done. Now we should have everything in place to get going.

Avatar gpichler said...

well dunn, dude!
sounds very useful :)

Mon Apr 21 20:54:29 +0200 2008
Anonymous Jan Foeh said...

Thanks for the writeup! That’s a bookmark.

The only thing I’d like to add is that you might want to consider filtering usernames for terms like ‘www’, ‘ftp’ etc when using subdomains as identifiers in order to prevent your users from monkeying around too much :)

Mon Apr 21 21:14:17 +0200 2008
Anonymous jack bedford said...

Thanks! My team was just about to embark on this exact task. Great writeup.

Tue Apr 22 03:28:34 +0200 2008
Anonymous Todd said...

Could this be used to support multiple domains with one application?

Each user getting their own URL if they wanted it?

Nice writeup.

Thu May 29 16:10:28 +0200 2008
Anonymous Ric said...

Also see Dan Webb’s routing plugin

Tue Jul 01 12:06:48 +0200 2008
Avatar S2 said...

@Todd, yes of course. That’s the point.

Sat Jul 05 12:17:26 +0200 2008
Anonymous Daniel Kehoe said...

It’s much easier to implement subdomains in Rails 3 than in Rails 2 (no plugin required). For anyone looking for a complete example implementation of Rails 3 subdomains with authentication (and a detailed tutorial) here’s my repo on Github:
http://github.com/fortuity/rails3-subdomain-devise
also Ryan Bates has a Railscast on Subdomains in Rails 3: http://railscasts.com/episodes/221-subdomains-in-rails-3

Tue Sep 07 22:17:02 +0200 2010
 
Comments for this post have been disabled