Adding support for custom domains in Rails
A fellow villager 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:
- Have your users add a CNAME record pointing their domain to yours
- Update your routes to handle any custom domains.
- 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:
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:
- 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
- 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:
- A user visits blog.company.com , which points to myapp.com
- Your app handles the request from the
custom
subdomain, routing the request to the#show
action inBlogsController
- 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.
Thanks for sharing, Kyle.
I’m curious what are you using as your webserver? And how do you handle SSL certificates?
I’m trying to figure out how to provision certificates and configure my Nginx when a new domain is configured.
Hey Geronimo, I hope you found the solution but I’ll comment just in case someone else is wondering the same as you.
For handling SSL certificates you can go a few ways.
If you want to build and manage the certificate management yourself, I’d recommend using Caddy (https://caddyserver.com/).
Alternatively, I built https://saascustomdomains.com which handles all the certificate management complexity for you. SaaS Custom Domains is truly a great product and I’m very proud to have built it. It lets you implement full custom domains functionality in less than 20 minutes.
I hope this helps someone.