Raking /etc/hosts For Sweeter Subdomainage

In a certain application I’m working on, each account and each site added in that account get a unique subdomain. Last time, I talked about making sure to reserve a few subdomains for yourself. This time we’ll talk about automating a chore that comes along with developing a subdomained application.

example.com

Did you know that example.com is a reserved domain? Yep, no one can register it. This means that you can use it for development locally if you want your development addresses to feel more production-like (rather than using localhost).

The easiest way to make this work on a mac is to edit your /etc/hosts file. If you open up that file, you can point example.com to localhost by adding the following line of code to the file.

127.0.0.1 example.com

Now you can visit http://example.com:3000 in your browser and you’ll hit the app that is running locally on port 3000 (assuming you have one running).

So what does this have to do with your fancy subdomains? Well, you can separate entries by a space and put multiple example.com subdomains in /etc/hosts.

127.0.0.1	localhost example.com www.example.com foobar.example.com

Now you can visit www.example.com:3000 or foobar.example.com:3000 in your browser and it will just work. This is really handy for manually testing your great new slice of subdomainage.

The Problem

Sweet you’re thinking, so you dig in and start creating subdomained sweetness. Once you get your signup form done, you decide to test it out. Let’s see, put in my email, select my monthly plan and I’ll use orderedlist as the subdomain. You hit the ‘Signup’ button and you are redirected to orderedlist.example.com, where you promptly receive the “can’t find server” error page. What? Well, you didn’t add it to your /etc/hosts file. Remember you added www and foobar but not orderedlist.

What a pain. For an app that just uses subdomains as simple account keys, you could just add a few to your /etc/hosts file and always remember to use those when testing your app. No biggie.

What I’m working on though uses subdomains for a few things. It’s a multi-site application, which means customers will be able to point their domain at it and the app will know how exactly how to deal with that. DNS changes like this can sometimes take time to resolve. I didn’t want a customer waiting for their DNS to go through to use the application, so each site also has a special subdomain based on the account that will work right away until DNS propagates. This means they can get in and work on their sites before flipping the DNS switch, if the site already exists on the interwebs. Also a sidebar, it will make https for the administration area possible, but that is a sidebar.

The Solution

So I was getting a little tired of editing the /etc/hosts file and decided to automate the process a bit with a rake task. Here is the task.

namespace :harmony do
  desc "Adds the necessary hosts to your /etc/hosts file"
  task :hosts => :environment do
    default, hosts = %w[www.example.com test.example.com], []
    
    # add all the site temporary domains
    hosts << Site.find(:all).inject([]) do |collection, site|
      collection << site.harmony_url
    end
    
    # add all the account subdomains
    hosts << Account.find(:all).inject([]) do |collection, account|
      collection << account.harmony_url
    end
    
    harmony_host_line = "127.0.0.1 " + hosts.flatten.sort.unshift(default).join(' ')
    lines = rio('/etc/hosts').readlines.reject { |l| l =~ /www\.example\.com\stest\.example\.com/ } << harmony_host_line
    rio('/tmp/hostscopy') < lines
    %x[sudo -p "Password:" cp /tmp/hostscopy /etc/hosts]
  end
end

Now when I run rake harmony:hosts, my host file is updated from live data in the application. I’ll walk you through the details.

First, I setup the defaults that I always want to be used no matter what data is in the database. Then I loop through all the items that have subdomains (sites and accounts), building up the hosts array.

default, hosts = %w[www.example.com test.example.com], []
    
# add all the site temporary domains
hosts << Site.find(:all).inject([]) do |collection, site|
  collection << site.harmony_url
end
    
# add all the account subdomains
hosts << Account.find(:all).inject([]) do |collection, account|
  collection << account.harmony_url
end

Once all that is done, I build the line that needs to be put into my hosts file.

harmony_host_line = "127.0.0.1 " + hosts.flatten.sort.unshift(default).join(' ')

Now that I have the line I need in my hosts file, I get all the lines in my current host file, rejecting the one that starts with my defaults, as I want to replace that line with the line created above.

lines = rio('/etc/hosts').readlines.reject { |l| l =~ /www\.example\.com\stest\.example\.com/ } << harmony_host_line

The code above uses a library called rio. It makes manipulating files and such as easy as fooling around with strings and arrays. Now that I have all the lines that will make up my new /etc/hosts file, I put them in a tmp file and then copy that over my current file forcing a sudo along the way.

rio('/tmp/hostscopy') < lines
%x[sudo -p "Password:" cp /tmp/hostscopy /etc/hosts]

So that is pretty much it. This becomes really handy when I pair it with loading my fixtures. I know, I know, fixtures suck. In this instance, I’m actually not minding them. Below is the rake task to load my fixtures into my development database (I’m using rSpec thus the spec namespace) and then run the hosts task to refresh my /etc/hosts.

namespace :harmony do
  desc "Does all the stuff to make it so you can run harmony."
  task :refresh => :environment do
    %w[spec:db:fixtures:load harmony:hosts].each do |task|
      Rake::Task[task].invoke
    end
  end
end

Now I can run rake harmony:refresh and my database is chock full of sample data and my /etc/hosts file is up to date based on that data.

A new person to the project could now git clone, rake db:migrate, rake harmony:refresh and have a nice setup of the app to browse around in.

9 Comments

  1. Nice tip! Had some thoughts on the code though.

    Using collect instead of inject would be more readable in my opinion:
    hosts << Site.find(:all).collect { |site| site.harmony_url }

    Or if you fancy the symbol to proc hack it could be even shorter like:
    hosts << Site.find(:all).collect &:harmony_url

    Also if you still want to use inject you could inject the host array like this to get rid of hosts <<:
    Account.find(:all).inject(hosts)…

  2. Something else that’s pretty helpful:

    .local is an unrouted Top Level Domain. Unless you’re authoritative for the Second Level Domain with a .local TLD, you should never ever get an answer for *.local

    I’m trying to implement something like this at work, and I have a few more pieces of the puzzle to complete before I’ll be able too.

    Still helpful for in-office private/internal purposes.

    Not exactly the same use-case as you have, but similar enough and good to know :).

  3. Hmmm, I just learned something new myself. It looks like what I’m doing might be a little bit off base according to the .local wikipedia article…

    At any rate, this template ( http://en.wikipedia.org/wiki/Template:Generic_top-level_domains ) is really helpful at a glance.

    I especially like fact that I’ve now seen .example, .local, .test, and .invalid, those could generally come in real handy.

  4. @David – Yeah, nice catch. I was doing something different and changed it so I forgot to look back over the code once I had it work to see if I could tweak stuff.

  5. Eric the Read Eric the Read

    Jun 25, 2008

    Personally, I don’t like creating entries in /etc/hosts, because that has all sorts of consequences that might not be desirable, like allowing other services to know the host exists.

    Instead, I just create a file called “mydomainproxy.pac” (or whatever), and put in it something like:

    function FindProxyForURL(url, host) {
    // All requests should go to their specified
    // destination by default
    var proxy_setting = “DIRECT

    if (host.match(new RegExp(“mydomain.com$”))) {
    proxy_setting = “PROXY localhost:3000”
    }
    return proxy_setting

    }


    Tell any browser to use that proxy file, and poof, now your rails app knows about any number of hosts under mydomain.com, but nothing else on your box has to.


    Of course, if you want other programs to know about your subdomains, then, um, nevermind. :)

  6. Eric the Read Eric the Read

    Jun 25, 2008

    ugh, sorry about the formatting. :(

  7. @Eric – Interesting idea but then you have to do it for each browser.

  8. Eric the Read Eric the Read

    Jun 26, 2008

    @John — True, but only once. After that, whenever you change your proxy file, just reload it or restart your browser, and it’s there. It also has the advantage that anyone can do it without needed admin rights on the machine in question.

    Of course, I’ve always had admin rights on my local box, but that’s not always true everywhere… Anyway, I didn’t mean to suggest your way was bad or anything, just wanted to provide another option.

  9. I had to do something similar, so I just installed named (or any dns would work) on my local machine, and set up a wildcard CNAME entry. So *.example.com aliases to example.com (which is an alias to localhost).

Sorry, comments are closed for this article to ease the burden of pruning spam.

About

Authored by John Nunemaker (Noo-neh-maker), a web developer and programmer who has fallen deeply in love with Ruby. More about John.

Syndication

Feed IconRailsTips Articles - An assortment of howto's and thoughts on Ruby and Rails.

Feed IconRails Quick Tips - Ruby and Rails related links.