How To Manage Multiple Domains & Subdomains with Rails 4 on Heroku

May 6, 2015   

Recently I've been working on a multi-tenant blog hosting application with a Rails front end, and this required us to let users set their own domains. Here's how to dynamically route your Rails application with multiple domains and subdomains (of yours and theirs).

Dynamic routing in Rails

In my application there is the concept of a Site which always has a slug, which becomes slug.mydomain.com. And sometimes has a domain, which belongs to them.

What we need to do is match the request based on a constraint which is simply a class we create, which implements the matches? method. This is very simple as you will see below:

routes.rb

...
class CustomDomainConstraint  
  # Implement the .matches? method and pass in the request object
  def self.matches? request
    matching_site?(request)
  end

  def self.matching_site? request
    # handle the case of the user's domain being either www. or a root domain with one query
    if request.subdomain == 'www'
      req = request.host[4..-1]
    else
      req = request.host
    end

    # first test if there exists a Site with a domain which matches the request,
    # if not, check the subdomain. If none are found, the the 'match' will not match anything
    Site.where(:domain => req).any? || Site.where(:slug => request.subdomain).any?
  end
end

match '/', :to => 'sites#index', :constraints => CustomDomainConstraint, via: :all  
match '/category/:slug' => 'sites#category', :constraints => CustomDomainConstraint, via: :all  
match '/:slug', to: 'sites#show', :constraints => CustomDomainConstraint, via: :all  
...

It's worth noting here that if your app allows users to specify their own domains, you should have a system of blacklisting your own domain/s and the www subdomain/slug.

Then all I need to do in the sites_controller.rb is implement a before_filter which finds the Site based on some request data, this is the code I used:

before_filter :find_site

...

private

  def find_site
    # generalise away the potential www. or root variants of the domain name
    if request.subdomain == 'www'
      req = request.host[4..-1]
    else
      req = request.host
    end

    # first test if there exists a Site with the requested domain,
    # then check if it's a subdomain of the application's main domain
    @site = Site.find_by(domain: req) || Site.find_by(slug: request.subdomain)

    # if a matching site wasn't found, redirect the user to the www.<root url>
    redirect_to root_url(subdomain: 'www') unless @site
  end

Pointing Domains And Subdomains To Your Rails Application

All you need to do, or have your users do is CNAME their domain name to your main domain which points to your app.

Multiple Domains On Heroku With Rails

My application lives on Heroku, and they use virtual hosts to route domain names, this means we need to tell them when we want to CNAME a new domain to our instance. To do this I just implemented an after_save on my Site model which uses Heroku's API to register a new domain with my application.

site.rb

after_save do |site|  
  heroku_environments = %w(production staging)
  if site.domain && (heroku_environments.include? Rails.env)
    added = false
    heroku = Heroku::API.new(api_key: ENV['HEROKU_API_KEY'])
    heroku.get_domains(ENV['APP_NAME']).data[:body].each do |domain|
      added = true if domain['domain'] == site.domain
    end

    unless added
      heroku.post_domain(ENV['APP_NAME'], site.domain)
      heroku.post_domain(ENV['APP_NAME'], "www.#{site.domain}")
    end
  end
end  

I was looking around the internet for a while before I figured out how to do this, so I hope this makes your day easier :) Comment below or tweet me if you have any questions or suggestions!