DaveSouth.org

Email RSS Twitter Delicious

Eliminate subdomains and CNAMEs in Rails development

This method is out-of-date. For a far better way please read Rails subdomains, CNAMEs, and crzy.me

Having two methods of identifying the same edition on a CMS — subdomains and CNAMEs — is painful. Yet many websites have advocated this approach. Every request to the application requires multiple checks on the domain to correctly identify the edition. I believe this process developed from the difficulties of dealing with subdomains and domains in the development environment. Recently we found the answer that simplifies the whole system.

Subdomains work

There are many instances where subdomains in an application make perfect sense. Basecamp and Lighthouse all use subdomains for account identification. It’s an elegant solution where you have a master domain example.com and accounts accountname.example.com.

Rails 3 has subdomain routing built-in. It’s trivial to forward all subdomain requests to the proper controller.

Subdomain development stinks

However, when developing for subdomain accounts you need a way to address them on the development box. You can’t use the full domain like accountname.example.com since it resolves to your production application. You can use accountname.localhost:3000 IF you add accountname to /etc/hosts.

This is what makes subdomains painful. Every new subdomain you want to test requires editing /etc/hosts. You can set up a local DNS server to resolve *.example.local to localhost. But it’s yet another layer of complexity on an already complex problem.

However, there is a better way. In Hey, PAC man. Sup, subdomains!, Taylor Luk outlines how to use Proxy autodiscovery to automagically manage subdomains. It works well and after some experiments, we’ve got a system that did something even more useful.

Subdomains fail

The reason we used subdomains is to address the proper edition in the development environment — editionname.localhost:3000. We would edit /etc/hosts for every test edition. But in production we don’t want subdomains at all. We want the canonical domain of the edition — www.davesouth.org.

After switching to using proxy.pac I realized it could do more than just forward a subdomain — it could forward ANY domain as long as it ended with .dev.

Real domains

So why use subdomains at all? How can we pass in the full edition domain instead?

Obviously we can’t use www.davesouth.org, but we could use www.davesouth.org.dev. Proxy.pac forwards all .dev calls to localhost. All we need is a way to convert that back into www.davesouth.org for the rails application.

In my article about the rack app, CustomDomain, I outlined how we converted domains into subdomains on the production server. This time we throw that all out. No SubdomainFu gem. No CustomDomain rack.

Instead we wrote a tiny rack app for the development environment. It strips away the .dev suffix in any domain longer than two terms and hands the full domain to the local rails app. Now we only work with full domains for edition recognition.

Set up proxy.pac

First create the proxy.pac file. I put the file on a static webserver where all my machines can reach it. I’ll explain why I did this a little later.

// proxy.pac
function FindProxyForURL(url, host) {
  if (shExpMatch(host, "*.dev")) {
    return "PROXY local:3000";
  }
  if (shExpMatch(host, "dev")) {
    return "PROXY local:3000";
  }
  return "DIRECT";
}

Notice that I have it point to local instead of localhost. You will need to edit /etc/hosts one more time. This time add the line:

127.0.0.1 local

To set up proxy.pac on the Mac. Open System Preferences > Network > Advanced > Proxies. Select Automatic Proxy Configuration and input the URL (http://example.com/proxy.pac). This enables proxy support for Safari and Google Chrome.

In Firefox, open Preferences > Advanced > Network > Connection Settings and enter the URL

Local Domain rack

Now add LocalDomain rack middleware to the rails application. Create local_domain.rb in your middleware directory (lib/middleware):

# Local Domain
#
# Subdomains are preserved
# local => local
# example.test => example.test
#
# Longer domains are truncated
# example.com.local => example.com
# www.example.com.dev => www.example.com
# slc.utah.example.com.test => slc.utah.example.com
#
# Only load in development environment

class LocalDomain

  def initialize(app)
    @app = app
  end
  
  def call(env)
    host = env['SERVER_NAME']
    parts = host.split('.')

    if parts.length > 2
      env['HTTP_X_FORWARDED_HOST'] = [host, domain(parts)].join(', ')
    end

    @app.call(env)
  end

  def domain(parts)
    parts[0..-2].join('.')
  end
end

This is a really simplistic rack application. It passes all domains with only two terms. If there are three or more, it strips off the last term no matter what it is.

Add the test file, local_domain_test.rb, to test/unit.

require 'local_domain'
require 'rack/test'

class LocalDomainTest < Test::Unit::TestCase
  include Rack::Test::Methods

  def app
    mock_app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Good Morning!"] }
    app = LocalDomain.new(mock_app)
  end

  def test_local_pass_through
    get 'http://local:3000/'
    assert_nil last_request.env['HTTP_X_FORWARDED_HOST']
    assert_equal 'local', last_request.env['SERVER_NAME']
  end

  def test_subdomain_pass_through
    get 'http://example.local:3000/'
    assert_nil last_request.env['HTTP_X_FORWARDED_HOST']
    assert_equal 'example.local', last_request.env['SERVER_NAME']
  end

  def test_strips_off_last_domain_term
    get 'http://www.example.com.local/'
    assert_equal 'www.example.com.local, www.example.com', last_request.env['HTTP_X_FORWARDED_HOST']
    assert_equal 'www.example.com.local', last_request.env['SERVER_NAME']
  end
end

Add local_domain.rb to your load path. The last step is to enable LocalDomain for development use only. Edit the development.rb environment file and add:

config.middleware.use "LocalDomain"

And that’s it. Any request longer than two terms into Rails will strip the last term. If I submit www.davesouth.org.dev the .dev will be removed and rails will only see www.davesouth.org.

This is much closer to the true production environment. We were able to remove a rack application in production as well. Instead we only munge the domain in development.

To make domains work in production using this method you can point the domain as a CNAME to your application domain (www.davesouth.org CNAME example.com) or just point the domain to your server’s IP address.

Windows testing

I run Windows XP on VMWare Fusion to test applications in IE 6, 7 and 8 plus Windows Safari, Chrome, Firefox and even Opera.

To set up proxy.pac on VMWare Windows XP:

  1. Open the “command prompt”
  2. Run ipconfig
  3. Note the Default Gateway — mine is 192.168.40.2 — and exit command prompt.
  4. Edit the hosts file at c:\windows\system32\drivers\etc\hosts
  5. Add the gateway IP and point it to local
    • 192.168.40.2 local
  6. Open Internet Options from the control panel (usually under networking)
  7. Click Connections>LAN Settings…
  8. Check Use automatic configuration script and put the URL in the address (example.com/proxy.pac).
  9. Hit OK and return from those menus
  10. Open Firefox — Tools>Options>Advanced>Settings
  11. Check Automatic proxy configuration URL and input the URL to proxy.pac

That’s it. Pointing “local” to the VMWare internal gateway means you can run the Rails app in development on the Mac and still see the development website in VMWare. This configuration enables proxy.pac on Windows IE, Safari, Firefox, Chrome and Opera.

Now why did I use a webserver to serve proxy.pac? Simple. Every browser handled referencing proxy.pac as a file, except IE which barfed all over the proxy file. If it’s a standard, hosted URL, it works fine in IE.