Adding support for custom domains in Rails


A fellow villager recently asked me how we had implemented custom domain support on Rigor’s public status pages. I tried to find a decent resource that walked through the steps, but I noticed that it was hard to find relevant search results, so I figured I’d write about it.

To clarify, the question at hand is this:

As a Rails developer, how can I let my users point their custom domains to my app?

There are three main steps necessary for adding custom domain support to your Rails project:

  1. Have your users add a CNAME record pointing their domain to yours
  2. Update your routes to handle any custom domains.
  3. Add the controller logic to find resources using the custom domain

For this example, let’s assume your user wants to use their domain at blog.company.com to point to their blog hosted on your Rails app at myapp.com/blogs/:id.

Add a CNAME record

Although this step actually occurs last (once you’ve implemented the logic in your app), it serves as a more logical starting point for this walkthrough. Think about it: for a custom domain to go to your app, the first step is to connect the two domains. Adding a CNAME record does just that.

Have your user add a CNAME record for their domain pointing to your domain. For this example, we’ll point blog.company.com to your app domain myapp.com.

Here is what the CNAME record looks like on DNSimple: DNSimple CNAME setup

Update your routes

In order for your users to be directed to the right place when they visit their custom domain, you’ll need to update the routes in your Rails app. In this case, we can add another root route that sends requests to your BlogsController, but constrain it to the blog subdomain.

# config/routes.rb

# requests to custom.myapp.com should go to the blogs#show action
root to: 'blogs#show', constraints: { subdomain: 'blog' }

# keep your regular resource routing (you probably already have some version of this part in place)
resources :blogs

This would work fine for users using blog as their subdomain, but what if we want to support any custom subdomain? Enter advanced routing constraints.

Use advanced routing constraints

Rails advanced constraints allow for more powerful routing logic. In our case, we can use an advanced constraint to add support for any custom domain. To use an advanced constraint:

  1. Define an object that implements the matches? method:
# lib/custom_domain_constraint.rb

class CustomDomainConstraint
  def self.matches? request
    request.subdomain.present? && matching_blog?(request)
  end

  def self.matching_blog? request
    Blog.where(:custom_domain => request.host).any?
  end
end
  1. Pass the object to the constraint in your routes.rb:
root to: 'blogs#show', constraints: CustomDomainConstraint

# or use the newer constraint syntax
constraints CustomDomainConstraint do
  root to: 'blogs#show'
end

Add the controller logic

With the new CustomDomainConstraint in place, any request that has a subdomain and a matching Blog record will get routed to the BlogsController#show action. To finish our implementation, we need to add logic in BlogsController that finds the correct blog to render.

Assuming your Blog model already has a custom_domain field, adding the logic is easy:

# app/controllers/blogs_controller.rb

def show
  @blog = Blog.find_by(custom_domain: request.host)
  # render stuff
end

For this to work properly, your user will need to set their blog’s custom_domain to blog.company.com in your app. With that in place, the request flow looks like this:

  1. A user visits blog.company.com, which points to myapp.com
  2. Your app handles the request from the custom subdomain, routing the request to the #show action in BlogsController
  3. Your controller looks up the blog with blog.company.com as the custom_domain and renders it

And just like that, your Rails app now supports custom domains!

Note for Heroku users:

If you’re using Heroku to host your Rails app, you’ll need an additional bit of logic to make this work. Heroku’s routing requires every domain to exist as a ‘custom domain’ in your Heroku app’s settings. This post outlines a way to automate this step via Heroku’s API and Rails background workers.

Going above and beyond

Keep standard route support

To make sure the standard blog routes still work (/blogs/:id), make sure your BlogsController still supports finding blogs by id:

# app/controllers/blogs_controller.rb

def show
  @blog = Blog.find_by(custom_domain: request.host) || Blog.find(params[:id])
  # render stuff
end

To clean things up a bit, you might consider moving this into a before_filter:

# app/controllers/blogs_controller.rb

before_filter :find_blog, only: :show

private

def find_blog
  # find the blog by domain or ID
end

While these tweaks aren’t required for custom domains to work, they do improve the BlogsController logic to be cleaner and more intuitive.