Friday, January 4, 2008

A Guide to Deploying Rails Applications

This article describes the way I deploy my Rails applications.

Thanks to Ruby on Rails I can quickly help my customers. The speed of development with Rails and agile practices make my work really fast. The only time when I slow down a bit is when I create and deploy a new project. If I saved the time somehow I could spend this time solving business problems instead.
I decided to start with a documentation of my deployment/development process. Then, I automated and simplified all I could and had time to. The result is a semi-automatic process which helps me with creating applications. This process is specialised for my needs and it uses the tools that I found useful. It doesn't have to be good for your needs.
All the setup was focused on agility.
I love to check-in frequently and be able to see the changes immediately on the production server. That's why I use tools like Capistrano or Vlad.

A quick description of the tools I use:
  • Rails
    • I don't think I have to explain why :)
    • most of the time I use edge Rails
  • Mongrel
    • A standard way of deploying in the Rails world.
    • fast and stable
    • Easy to configure
  • nginx
    • an http server that is responsible for load balancing
    • easy to configure (even easier if you can read Russian blogs)
    • easy to handle many Rails applications.
  • Capistrano
    • A standard tool for easy deployment.
    • I use Vlad for some of my applications, both are good.
  • Piston
    • Easy plugins management
  • Subversion
  • mysql
Nginx configuration may be tricky when you work on a shared hosting but the main points stay the same also with Apache and other Http servers. Some of my applications are still on a shared machine (I recommend Webfaction), but most of them are on a dedicated machine.

Let's assume that you want to create and deploy a new Rails applications. The whole process can be described in 5 general steps.
  1. Create an SVN repository, create the app and import it to the repository.
  2. Use capistrano to make the remote work easier.
  3. Create the production database.
  4. Prepare mongrel_cluster to work with the new application.
  5. Configure nginx and bind the domain.

Ok, let's go into details.
  • Remote: Create a repository for the application.
    • svnadmin create /var/repos/app
      • I prefer to have a separate repo for each of my applications.
      • It's good to have one directory for all repositories
      • I use svn+ssh protocol, in my case it's easier to have just one source of users (Linux users)
  • Checkout the application to the /tmp/app directory
    • svn co $REMOTE_SVN/app /tmp/app
      • I recommend having environment variables for the svn paths, works nicely with shell autocompletion.
  • Use edge rails to create a rails app.
    • svn up ~/tmp/rails_edge
      • update it first
      • my rails_edge lives there, you can always create it with 'svn co $RAILS_DEV_TRUNK'
    • ruby ~/tmp/rails_edge/railities/bin/rails -dmysql /tmp/app/trunk
      • '-dmysql' because Rails has recently changed its default db to sqlite
    • svn ci -m "initial import"
  • Live on Rails edge
    • piston import $RAILS_DEV_TRUNK vendor/rails
    • svn ci -m "piston imported edge rails"
  • capify
    • capify .
    • It creates two files.
  • modify config/deploy.rb:

set :application, "app"
my_server = "12.34.56.7"
set :repository, "svn+ssh://#{my_server}/var/repos/app/trunk"
set :deploy_to, "/var/www/#{application}"
role :app, my_server
role :web, my_server
role :db, my_server, :primary => true

  • Commit.
    • svn ci -m "capistrano setup"
  • cap deploy:setup
    • it creates all the directories on the remote server
  • cap deploy:cold
    • it checkouts the code, creates a 'current' symlink
    • logs are in the 'shared' directory
    • it fails on db:migrate task which is fine for now.
  • Create the production database
    • Remote: Go to /var/www/app/current
    • Remote: rake RAILS_ENV=production db:create
  • Local: cap deploy:cold
    • Again.
    • It should pass the db:migrate now
    • But it fails on missing script/spin file
  • Create script/spin with the following content:
    • mongrel_cluster_ctl restart
    • Yes, you need mongrel_cluster on the remote server
    • Add this file to svn and commit
  • cap deploy:cold
    • This time it worked!
    • But... we didn't setup mongrel_cluster to restart our app.
  • Mongrel_cluster
    • Create config/mongrel_cluster.yml
---
cwd: /var/www/app/current
log_file: log/mongrel.log
port: 5000
environment: production
group: www-data
user: your-username
address: 127.0.0.1
pid_file: tmp/pids/mongrel.pid
servers: 3
  • Commit this file
  • cap deploy:cold
    • still doesn't work
    • Mongrel_cluster doesn't know that it should use this file
  • Remote: Go to /etc/mongrel_cluster/
  • Make a symbolic link to the yml file
    • sudo ln -s /var/www/app/current/config/mongrel_cluster.yml app.yml
  • cap deploy:cold
    • And it works!
    • you should now be able to connect to localhost:5000 from the remote server.
  • Now we will bind the domain to this mongrel_cluster.
    • We need to configure nginx.
    • I have setup nginx so that for every application I create a single file that is automatically loaded on nginx startup.
    • The file just needs to live in the /etc/nginx/vhosts/ directory.
    • It works because I have the following line in my /etc/nginx/nginx.conf file:
      • include /etc/nginx/vhosts/*.conf;
  • Create /etc/nginx/vhosts/app.conf

upstream app {
server 127.0.0.1:5000;
server 127.0.0.1:5001;
server 127.0.0.1:5002;
}
server {
listen 80;
client_max_body_size 50M;
server_name app.com www.app.com;
root /var/www/app/current/public;
access_log /var/www/app/current/log/nginx.log;

if (-f $document_root/system/maintenance.html) {
rewrite ^(.*)$ /system/maintenance.html last;
break;
}

location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect false;
proxy_max_temp_file_size 0;
if (-f $request_filename) {
break;
}
if (-f $request_filename/index.html) {
rewrite (.*) $1/index.html break;
}
if (-f $request_filename.html) {
rewrite (.*) $1.html break;
}
if (!-f $request_filename) {
proxy_pass http://app;
break;
}
}

error_page 500 502 503 504 /500.html;
location = /500.html {
root /var/www/app/current/public;
}
}

  • Restart nginx
    • sudo /etc/init.d/nginx stop
    • sudo /etc/init.d/nginx start
  • That's it :)
  • From now on you just write the code, commit and call capistrano to deploy.

I hope you found this guide useful. There is still a lot to improve and a lot of duplicates (like defining mongrel ports). Let me know if you think that I could do something in a better way.

Here is a list of articles that I found useful when I was experimenting with Rails deployment.

Installing and Configuring Nginx and Mongrel for Rails

Install Ruby Rails on Gutsy Gibbon (Nginx Version)


Hosting Rails apps using nginx and Mongrel Cluster

A clean slate, Edge Rails recipe

4 comments:

mileszs said...

Thanks, I think this will be helpful. I haven't done enough deep study of deployment, and have done most of it manually, either following how-tos or by trial and error. This will be a handy reference, especially when combined with the Nginx/Mongrel links at the end!

Anonymous said...

Deprec is really awesome for helping with lots of these tasks, imo (even despite its current lack of cap2 support)q

Andrzej Krzywda said...

miles: I think starting with manual deployment may be useful. It gives you better understanding what are the single tasks.

dr_nic: Deprec does look awesome, indeed! I wasn't aware of it. I was going to create little scripts for my deployment tasks but going the deprec way (capistrano recipes) sounds much better. Any idea when Cap2 will be supported?

Anonymous said...

I'm a newbie to Rails and I have a question that I haven't found a good answer for. Maybe you can help me out.

Is it possible to create a rails app and have it deployed to a specific directory on a web server? For instance, my company has a web site that is powered by a PHP CMS (Drupal), but I see in Rails ways I could create an set of web apps to add to our web site, but I'm not sure if this can be done.

With PHP, the process would be: write the app, upload to a directory, then point visitors to that directory to use the app.

From what I understand of Rails (which I fully admit I could have this all wrong), it seems like one needs to build the site in Rails as its own application. For instance, 37 signals' apps are all separate. Why they're not centrally located under one domain as part of a "suite" is beyond me.

Does that make sense?