By Dave South •
Rails subdomains, CNAMEs, and crzy.me
Setting up a Rails development environment to handle subdomains and custom domains is tricky. There are lots of suggestions from editing your /etc/hosts file, custom rack applications to handle CNAMEs, dnsmasq for wildcard domains, and more. All of them are compromises and often caused more problems than they solved. However, there is a decent solution to the problem.
Wildcard domains
The first problem is assigning a wildcard domain to your local machine. Using localhost:3000 for Rails is fine until you need a subdomain, or worse, a custom domain. Past suggestions to use dnsmasq or proxy pac only partially solve the problem and are patchy at best. Adding custom domains or subdomains to /etc/hosts is a pain.
I got tired of these solutions and finally just created crzy.me—a real domain mapped to 127.0.0.1. It’s a real, wildcard domain on the web that just points to localhost. Any subdomain like davesouth.crzy.me, always points back to my development environment.
Of course, I thought I was being really clever until I found that there are lots of localhost pointing domains—*.vcap.me and *.lvh.me are just two examples.
The point is that you don’t need to map anything to localhost on your own machine. Just use crzy.me or similar domains and forget the problem.
“But don’t you have to have an internet connection for it to work?”
Yes, but who develops software without a decent connection? It’s almost impossible. Invest in a good smartphone with tethering and stop worrying.
Subdomain admin, CNAME website
Now we need to handle incoming CNAME connections. At first I used subdomains on my local machine and a CustomDomain rack app to map incoming CNAME’s to the subdomain. The admin interface was under the subdomain URL with /admin path added. What a disaster!
Today I recommend using a subdomain for your administration interface. Then use CNAME directly for the public facing website. On the production server, no rack application is needed.
For our example, we’ll pretend example.com is our production server domain. For all administration functions we use something like davesouth.example.com. All we have to do is check the route and ensure it has example.com in the domain and we stay in the admin interface.
When we send traffic using a CNAME like www.davesouth.org, the Rails routing sees it’s not directed to example.com and routes to the public controllers on the application. To do this we need two custom constraints in our routes.
Site host
First you need to identify the root domain based on your environment. You can use environment variables if you wish. For me I use a YAML file and assign it to a Site object along with other useful site wide defaults.
# config/site.yml
defaults: &defaults
title: APPLICATION NAME
tagline: APPLICATION TAG LINE
other_common_site_wide_param: ...
production:
<<: *defaults
domain: example.com
development:
<<: *defaults
domain: crzy.me
test:
<<: *defaults
domain: example.test
Then load the YAML file into Site.
# config/initializers/_site.rb
# Load site yaml file
site_config = File.read Rails.root.join('config', 'site.yml')
# Enable to parse yaml with ERB
# site_config = ERB.new(site_config).result
# Select environment
site_config = ActiveSupport::HashWithIndifferentAccess.new(YAML.load(site_config)[Rails.env])
# Setup Site object
Site = OpenStruct.new(site_config)
# Set ENV['DOMAIN'] to temporarily change development domain
# This is useful for debugging in VirtualBox. Use crzy.ws
# instead of crzy.me.
Site.domain = (ENV['DOMAIN'] or Site.domain) if Rails.env.development?
# Split domain
Site.host, Site.port = Site.domain.split(':')
Constraint methods
We also need methods to determine if a route is a subdomain or CNAME domain.
# config/initializers/route_constraints.rb
class CnameRoute
def self.matches?(request)
request.host !~ /^[a-z0-9\-\.]*#{Site.host}/
end
end
class SubdomainRoute
def self.matches?(request)
request.host =~ /^[a-z0-9\-\.]*#{Site.host}/ && request.subdomain.present? && request.subdomain != 'www'
end
end
Constrained routing
And finally add the constraints to the routes file.
# config/routes.rb
# Identify incoming CNAME routes using CnameRoute method
constraints(CnameRoute) do
# Route topics traffic to public controllers under themes
match '/topics' => 'themes/topics#listing', as: :theme_topics
...
root to: 'themes/topics#display', as: :theme_root
end
# Identify incoming subdomain routes using SubdomainRoute method
constraints(SubdomainRoute) do
root to: redirect('/posts')
...
# Route admin topic to private resource based controllers
resources :topics
end
Using CNAMEs on the development machine
Now we have subdomain and CNAME routing that works perfectly for the production server. We are left with one final problem, using CNAME addresses on the development machine. Obviously I can’t direct www.davesouth.org to localhost.
It turns out that there is a simple solution. Use crzy.me as a suffix for the CNAME. So instead of www.davesouth.org we use www.davesouth.org.crzy.me. All we need is a little rack application to strip off crzy.me before handing it to the application.
First put this rack application in your load path.
# Local Domain
#
# A simple method to hand full domains into test,
# development or staging environments for the
# application to scope by domains without requiring
# complicated workarounds before production.
#
# It strips off the last two terms of the host domain
# if it is more than three terms long.
#
# For staging environments it's useful to test the
# full domain before going live in production.
#
# Also useful for production environments if you wish
# to provide URLs to domains not ready to go live.
#
# For development machines, use a wildcard domain that
# resolves to localhost -- crzy.me is one I've set up.
#
# Subdomains are preserved;
# crzy.me => crzy.me
# subdomain.crzy.me => subdomain.crzy.me
#
# Longer domains are truncated and passed to the app
# www.example.com.crzy.me => www.example.com
#
#
# Ensure LocalDomain is in the Rails load path
#
# Add this to the correct environment file:
# config.middleware.use 'LocalDomain'
class LocalDomain
def initialize(app)
@app = app
end
def call(env)
host = env['SERVER_NAME']
parts = host.split('.')
if parts.length > 3
env['HTTP_X_FORWARDED_HOST'] = [host, domain(parts)].join(', ')
end
@app.call(env)
end
def domain(parts)
parts[0..-3].join('.')
end
end
Second, add this line to production.rb and development.rb
config.middleware.use 'LocalDomain'
.
Note that Local Domain will strip off the last two terms of a domain if the domain is more than three terms long. It converts www.davesouth.org.crzy.me to www.davesouth.org before handing it off to the Rails application.
The Rails app will see the CNAME as the host domain just as if it were on the production machine.
Secure your admin section
One huge advantage of this setup is how easy it is to secure your admin interface. By using subdomains on a common domain for the admin you can secure your admin interface using a wildcard SSL certificate. In our example, we would use *.example.com as the SSL certificate. Currently RapidSSL sells basic wildcard certificates for $129 per year.
Production use
So now we have a multiple account system using subdomains for the admin and CNAMEs for the public facing sites. We can address the CNAME sites from our development machine. We can secure the admin section using SSL. And we don’t need some crazy set up locally to make it work.
There is also another advantage using the local domain rack application. On the production server you can set up placeholder CNAME accounts that are not ready for public use. Just point to www.davesouth.org.example.com to see the site under development. When it’s ready for publication, set www.davesouth.org to point to your server and you are online.