Deploying Sinatra On Ubuntu: In Which I Employ A Secretary

As mentioned previously, I really hate getting woken up at 3 AM in the morning.  This happens fairly frequently for me, though, because I live in Japan and about half of the people who call me do not. I have not been effective at getting them to check what time it is here before they call, but I certainly want them to call, and even call me in the middle of the night if it is an emergency.

So I made myself a phone secretary with Twilio, their Ruby gem, and Sinatra (a lightweight Ruby web framework).  I gave my friends and family a US number assigned to me by Twilio. Dialing it causes Twilio’s computer to talk to my server and figure out what I want to do with the call. The server runs a Sinatra app which checks the time in Japan and either forwards the call to the most appropriate phone or gently informs the user that it is 4:30 AM in the morning.

The code for this took 10 minutes. Reasoning my way through a deployment took, hmm, 3 hours or so. I am a programmer not a sysadmin, what can I say. I thought I’d write down what I did so that other folks can save themselves some pain.

Code (You’re probably not too interested in the exact logic, but feel free to use it as a springboard if you want to make a secretary/call forwarding app):

require 'rubygems'
require 'sinatra'
require 'twiliolib'
require 'time'

@@HOME = "81xxxxxxxxxx"  #This is not actually my phone number.
@@CELL = "81xxxxxxxxxxxx"  #Neither is this.

def pretty_time(time)
time.strftime("%H:%M %p")
end

def time_in_japan()
time = Time.now.utc
time_in_japan = time + 9 * 3600
end

def is_weekend?(time)
(time.wday == 0) || (time.wday == 6)
end

def in_range?(t, str)
time = Time.parse("#{Date.today.to_s} #{pretty_time(t)}")
range_bounds = str.split(/, */)
start_time = Time.parse("#{Date.today.to_s} #{range_bounds[0]}")
end_time = Time.parse("#{Date.today.to_s} #{range_bounds[range_bounds.size - 1]}")
(time >= start_time)  && (time  "45")
@r.append(say)
@r.append(call)
@r.respond
end

def redirect_twilio(url)
@r = Twilio::Response.new
rd = Twilio::Redirect.new("/#{url.sub(/^\/*/, "")}")
@r.append(rd)
@r.respond
end

post '/phone' do
t = time = time_in_japan
if (is_weekend?(time))
if in_range?(time, "2:00 AM, 10:00 AM")
redirect_twilio("wakeup")
else
forward_call(@@CELL)
end
else #Not a weekend.
if in_range?(time, "2:00 AM, 8:30 AM")
redirect_twilio("wakeup")
elsif in_range?(time, "8:30 AM, 6:30 PM")
redirect_twilio("working")
elsif in_range?(time, "6:30 PM, 9:00 PM")
forward_call(@@CELL)
else
forward_call(@@HOME)
end
end
end

post '/wakeup' do
if (params[:Digits].nil? || params[:Digits] == "")
@r = Twilio::Response.new
say = Twilio::Say.new("This is Patrick's computer secretary.  He is asleep right now because it is #{pretty_time(time_in_japan)}.  If this is an emergency, hit any number to wake him up.")
g = Twilio::Gather.new(:numDigits => 10)
g.append(say)
@r.append(g)
@r.respond
else
forward_call(@@HOME, true)
end
end

post '/working' do
if (params[:Digits].nil? || params[:Digits] == "")
days_left = (Date.parse("2010-04-01") - Date.today).to_i
@r = Twilio::Response.new
say = Twilio::Say.new("This is Patrick's computer secretary.  He is at work as it is #{pretty_time(time_in_japan)}.  Only #{days_left} days left!  If this is an emergency, hit any number call him at work.")
g = Twilio::Gather.new(:numDigits => 10)
g.append(say)
@r.append(g)
@r.respond
else
forward_call(@@CELL, true)
end
end

get '/' do
'Hello from Sinatra!  What are you doing accessing this server anyway?'
end

This script is a bit ugly but, hey, what do you want in ten minutes. (Memo to self: correct it after leaving my job.)

Sinatra Deployment On Ubuntu

A quick look around the Internet didn’t show any cookbook recipes for deploying Sinatra. I thought I’d write up what I’m using, which uses Apache reverse proxying to Sinatra. (Instructions included for Nginx as well.) It assumes you already have your webserver running and are familiar with basic Ruby usage and the Linux command line.

1) Install the daemons gem. We’re going to daemonize Sinatra so that it runs out of our console and starts and stops without our intervention, much like Apache does.

2) Create an /opt/pids/sinatra directory. (It seemed as good a place as any.) Let a non-privileged user write to that directory, for example by executing “sudo chown www-data /opt/pids/sinatra; sudo chmod 755 /opts/pids/sinatra”. Make a note of what non-privileged user you use. I am just reusing www-data because Apache has conveniently provided him for me and he is guaranteed to not to be able to screw up anything important if he is compromised.

2) Write a quick control script and put it in the same directory as your Sinatra app (called phone_sinatra.rb for the purposes of this demonstration). I threw these in /www/var/phone.example.com/ but you can put them anywhere. Make sure the scripts are readable, but not writable, by www-data. (sudo chmod 755 /www/var/phone.example.com/ will accomplish this: it makes only the owner able to write to it, but any user on the system — including www-data — can read from it.)

require 'rubygems'
require 'daemons'

pwd = Dir.pwd
Daemons.run_proc('phone_sinatra.rb', {:dir_mode => :normal, :dir => "/opt/pids/sinatra}) do
Dir.chdir(pwd)
exec "ruby phone_sinatra.rb"
end

3) (Optional) Add in a reverse proxy rule to Apache or Nginx to send requests to the subdomain of your choice to Sinatra instead. I ended up deploying this through Apache, so the rule is pretty quick:


ServerName phone.example.com

ProxyPass / http://phone.example.com:4567/

You could also do this on Nginx and it is similarly trivial.

server {
listen       80;
server_name phone.example.com;
proxy_pass http://phone.example.com:4567/;
}

The main reason I do this is to not have to remember non-standard ports in my URLs. It also simplifies firewall management if you’re into that sort of thing.

4) Add a control script to /etc/init.d/sinatra so that we can start and stop Sinatra just like we do other services, like Apache.

#!/bin/bash
#
# Written by Patrick McKenzie, 2010.
# I release this work unto the public domain.
#
# sinatra      Startup script for Sinatra server.
# description: Starts Sinatra as an unprivileged user.
#

sudo -u www-data ruby /var/www/phone.example.com/control.rb $1
RETVAL=$?

exit $RETVAL

5) Tell Ubuntu to start your daemon when the computer starts up and shut it off when the computer starts down: sudo update-rc.d sinatra defaults

6) Start the service manually for your first and only time: sudo /etc/init.d/sinatra start

There you have it: Sinatra is running the application you wrote, and it will start and stop with your Ubuntu server. If you were doing this for Twilio now you’d check your Twilio account settings to make sure it has the right URL set up for your phone number, and then try calling yourself. Preferably NOT from the phone you try to forward to.

All code in this blog post was written by Patrick McKenzie in early 2010. I release it unto the public domain. Feel free to use it as the basis for your own apps.

Twilio development makes me feel like a kid in a candy store — you can affect the real world through an API, how cool is that? I think next time I have a few hours to kill I’m going to make a similar secretary for my business. I don’t give folks my phone number because a) I live in Japan and b) they don’t pay me enough to do telephone support. However, quoting a telephone number on your website instantly says “There is a real business behind this!”

I think I’ll whip up a computer secretary for the business which handles the most common two support requests (“I didn’t get my Registration Key” and “I lost my password.”), and for anything else takes their message and emails it to me. That sort of thing costs megacorporations bazillions and can be whipped up these days by a single programmer on Saturday morning for under $5 a month in operating costs. Like I said, candy store.

def pretty_time(time)
time.strftime(“%H:%M %p”)
end

def time_in_japan()
time = Time.now.utc
time_in_japan = time + 9 * 3600
end

def is_weekend?(time)
(time.wday == 0) || (time.wday == 6)
end

def in_range?(t, str)
time = Time.parse(“#{Date.today.to_s} #{pretty_time(t)}”)
range_bounds = str.split(/, */)
start_time = Time.parse(“#{Date.today.to_s} #{range_bounds[0]}”)
end_time = Time.parse(“#{Date.today.to_s} #{range_bounds[range_bounds.size - 1]}”)
(time >= start_time)  && (time < end_time)
end

def forward_call(number, surpress_intro = false)
@r = Twilio::Response.new
say = Twilio::Say.new(“#{“This is Patrick’s computer secretary.  ” unless surpress_intro}I’m putting you through.  Wait a few seconds.”)
call = Twilio::Dial.new(number, :timeLimit => “45″)
@r.append(say)
@r.append(call)
@r.respond
end

def redirect_twilio(url)
@r = Twilio::Response.new
rd = Twilio::Redirect.new(“/#{url.sub(/^\/*/, “”)}”)
@r.append(rd)
@r.respond
end

post ‘/phone’ do
t = time = time_in_japan
if (is_weekend?(time))
if in_range?(time, “2:00 AM, 10:00 AM”)
redirect_twilio(“wakeup”)
else
forward_call(@@CELL)
end
else #Not a weekend.
if in_range?(time, “2:00 AM, 8:30 AM”)
redirect_twilio(“wakeup”)
elsif in_range?(time, “8:30 AM, 6:30 PM”)
redirect_twilio(“working”)
elsif in_range?(time, “6:30 PM, 9:00 PM”)
forward_call(@@CELL)
else
forward_call(@@HOME)
end
end
end

post ‘/wakeup’ do
if (params[:Digits].nil? || params[:Digits] == “”)
@r = Twilio::Response.new
say = Twilio::Say.new(“This is Patrick’s computer secretary.  He is asleep right now because it is #{pretty_time(time_in_japan)}.  If this is an emergency, hit any number to wake him up.”)
g = Twilio::Gather.new(:numDigits => 10)
g.append(say)
@r.append(g)
@r.respond
else
forward_call(@@HOME, true)
end
end

post ‘/working’ do
if (params[:Digits].nil? || params[:Digits] == “”)
days_left = (Date.parse(“2010-04-01″) – Date.today).to_i
@r = Twilio::Response.new
say = Twilio::Say.new(“This is Patrick’s computer secretary.  He is at work as it is #{pretty_time(time_in_japan)}.  Only #{days_left} days left!  If this is an emergency, hit any number call him at work.”)
g = Twilio::Gather.new(:numDigits => 10)
g.append(say)
@r.append(g)
@r.respond
else
forward_call(@@CELL, true)
end
end

get ‘/’ do
‘Hello from Sinatra!  What are you doing accessing this server anyway?’
end

No Responses to “Deploying Sinatra On Ubuntu: In Which I Employ A Secretary”

  1. Jack Dempsey January 16, 2010 at 5:50 am #

    Cool stuff Patrick. I’m guessing the double definition of pretty_time is a copy/paste typo?

  2. Ken Hoxworth January 16, 2010 at 7:11 am #

    You should take a look at Passenger and their documentation on deploying a Rack-based application. It makes deploying a Sinatra web app stupid simple on apache, without any proxy forwarding and the application is started/stopped with apache, removing the need to create a startup script.

  3. Marc January 16, 2010 at 7:48 am #

    Listen to Ken Hoxworth!
    Just wanted to add that your sinatra app could also run using passenger on nginx. Passenger supports both.
    You usually only have to point passenger to the public directory of your app and create a rackup file (config.ru) like this:

    require ‘rubygems’
    require ‘sinatra’
    require ‘main.rb’
    run Sinatra::Application

  4. Felipe Coury January 16, 2010 at 8:42 am #

    You can also Deploy it to Webbynode without all the hassle:

    http://blog.webbynode.com/rapid

    Worth a look :)

  5. Noah January 16, 2010 at 8:55 am #

    Awesome script. As others suggested, Rack is the ‘right’ way to interface with a webserver. The, take a look at Heroku for no-hassle deployment and hosting:

    http://docs.heroku.com/rack

  6. Ade January 16, 2010 at 9:06 am #

    This is very cool, but did it really take just 10 minutes to code? I find that hard to believe.

  7. Mike H January 16, 2010 at 11:36 am #

    I highly recommend heroku. You can be deployed in 5 minutes, and it’s free for the lowest level of processing power.

  8. Brent Rockwood January 17, 2010 at 10:48 am #

    +1 to Heroku. I deploy test apps to it all the time in 10 minutes or less — 8 minutes of which involves fighting with my DNS.

  9. Rich January 18, 2010 at 9:15 am #

    I think that actually the coolest part of this is that you are not *employing* a secretary, you’re *deploying* a secretary.

  10. Matt December 9, 2010 at 4:12 pm #

    This post helped me, so thanks! One difference I noted while setting this up though was that the proxy_pass directive has to live inside a location block. Here’s what worked for me:

    server {
    listen 80;
    server_name phone.example.com;
    location / {
    proxy_pass http://phone.example.com:4567/;
    }
    }

Trackbacks/Pingbacks

  1. MicroISV on a Shoestring - Deploying Sinatra On Ubuntu: In Which I Employ A Secretary - Hear a Blog - January 16, 2010

    [...] http://www.kalzumeus.com/2010/01/15/deploying-sinatra-on-ubuntu-in-which-i-employ-a-secretary/ [...]

  2. Productizing Twilio Applications | Kalzumeus Software - December 19, 2011

    [...] did a presentation about how to “productize” Twilio applications: to take them past the “cool weekend hack” stage and make them production-ready.  Twilio has graciously released videos of many of the [...]

Loading...
Free video + email advice on making & selling software:
(1~2 emails a week.)