Deploying Multiple Merb Apps Behind Nginx

May 18, 2008 | Author: glasner | Publisher: hassox

The easiest way I've found to deploy multiple Merb apps to the same server.

Are you using Merb to build smaller application that don’t need they’re own server? Same here. Merb has replaced Rails for all my internal tools, and below is the simplest method I’d found for deploying them onto a single server.

Nginx

On the front-end I have everyone’s favorite Russian export, Nginx. If you aren’t running Nginx already, you’re obviously going to need to install it. The list of options is pretty overwhelming, but I’ve found the following works well


./configure --prefix=/usr/local/etc/nginx --user=nginx --group=deploy --sbin-path=/usr/local/sbin/nginx --conf-path=/usr/local/etc/nginx/base.conf --pid-path=master.pid  --http-client-body-temp-path=/tmp/nginx/client_body --http-proxy-temp-path=/tmp/nginx/proxy --http-fastcgi-temp-path=/tmp/nginx/fastcgi --with-http_ssl_module --error-log-path=/www/logs/nginx/error.log --http-log-path=/www/logs/nginx/access.log --pid-path=master.pid --lock-path=master.lock 
make
make install

A lot of the above config options can be left out, but pay particular attention to the default config file (—conf-path). You can override the default when launching Nginx w/ an option (-c if you’re wondering), but it soo much easier to just type `sudo nginx`.

If you get any errors, you’re most likely missing the pcre or zlib libraries. You can placate the Russian beast by installing the development version of the library you need, but I’ve found it easier to just download the source and add the following options during configuration


--with-pcre=path_to_pcre_src --with-zlib=path_to_zlib_src

If you’re expecting a decent amount of traffic, you should also check out the fair upstream module

Base Nginx Configuration

A default config file is created for us during installation, but it has to be perfected. Instead of one file, I break config out into the following files:

  • base.conf – general settings
  • gzip.conf – how and what to gzip
  • logs.conf – log format and paths
  • mime.types – endless list of mime types

You can grab them at Github

base.conf


# user and group to run as
user nginx deploy;

# number of nginx workers
worker_processes  3;

# pid of nginx master process
pid master.pid;

# Number of worker connections. 1024 is a good default
events {
  worker_connections 1024;
}

# start the http module where we config http access.
http {

  include mime.types;

  include logs.conf;

  # no sendfile on OSX
  sendfile on;

  # fixes timeouts while processing large files
  proxy_read_timeout 600;  

  # These are good default values.
  tcp_nopush        on;
  tcp_nodelay       off;

  include gzip.conf;

  # Finds all Merb/Rails apps in /www/apps
  # Each app has to have nginx.conf file in config directory
  include /www/apps/*/current/config/nginx.conf;
}       

Most of the above is taken directly from Ezra’s config for Rails. The last line was inspired by rubypond and let’s nginx automatically discover new apps added to the server. If they have the magic conf file that is…

nginx.conf for your app

Every app that is deployed to your server needs to have nginx.conf in its config directory.


# replace app_name w/ your app's name
# replace app.com w/ your app's domain

upstream app_name {
  # replace 4000-4002 w/ the ports merb will be on
  server 127.0.0.1:4000;
  server 127.0.0.1:4001;
  server 127.0.0.1:4002;
}

server { 
  listen 80;
  server_name app.com;

  # write app specific log
  # make sure you create this file in your log directory before running behind nginx
  access_log  /www/apps/app_name/shared/log/nginx.log  main;

  # let nginx serve static files directly
  # images
  location ^~ /images {
    root /www/apps/app_name/current/public;
  }

  # javascript
  location ^~/javascripts {
    root /www/apps/app_name/current/public;
  }

  # css
  location ^~/stylesheets {
    root /www/apps/app_name/current/public;
  }           

  # Push all other requests to Merb
  location / {
    # needed to forward user's IP address to merb
    proxy_set_header  X-Real-IP  $remote_addr;

    if (!-f $request_filename) {
      proxy_pass http://app_name;
      break;
    }
  }

} 

I used to keep these files in /etc/nginx/hosts/ but having them in the config directory means they get deployed with the rest of the app.

Less clicks = more GTA!

Enter Capistrano

Capistrano is a lazy developer’s best friend. Unfortunately, it doesn’t play nicely with Ngnix or Merb. Let’s change that by editing deploy.rb…

For Nginx

First, we have to add a couple Nginx specific variables:


set :nginx_bin, 'user/local/sbin'
set :nginx_pid, '/usr/local/etc/nginx/master.pid'

Now some tasks:


namespace :nginx do

  desc "Reload nginx config files without restarting"    
  task :reload, :roles => :web do
    sudo "kill -HUP `cat #{nginx_pid}`" 
  end

  desc "Start"    
  task :start, :roles => :web do
    sudo "#{nginx_bin}/nginx" 
  end

  desc "Kill nginx"    
  task :stop, :roles => :web do
    sudo "kill `cat #{nginx_pid}`" 
  end

  desc "Restart nginx"    
  task :restart, :roles => :web do
    nginx.stop
    nginx.start
  end

end 

For Merb

The variables:


set :number_of_instances, 3
set :first_port, 5000
set :app_server, 'mongrel'

The tasks:


namespace :merb do

  desc "Start merb cluster"    
  task :start, :roles => :app do
    run "nohup merb -a #{app_server} -c #{number_of_instances} -m #{deploy_to}/current -p #{first_port} -e production"#" -h 127.0.0.1" -u #{user} -G #{group} 
  end

  desc "Stop merb cluster"    
  task :stop, :roles => :app do
    run "merb -m #{deploy_to}/current -k all" 
  end 

end    

Make nice

Setup MERB_ENV


set :migrate_env, “MERB_ENV=production”

Finally, we have to overwrite a few default tasks to get everything to work together:


# Overwrite main tasks

namespace :deploy do

  desc "Custom restart task for merb appserver" 
  task :restart do
    merb.stop
    merb.start
  end

  desc "Custom start task for merb" 
  task :start, :roles => :app do
    merb.start
  end

  desc "Custom stop task for merb cluster" 
  task :stop, :roles => :app do
    merb.stop
  end

end       

Now you’re ready to roll. Thank pluesch0r the next time your in #merb for the Merb tasks.

Deploy

Ready? Then…

    
cap deploy:setup
cap deploy
cap nginx:reload

Remember to use nginx:reload anytime you push a new version of nginx.conf for your app. Otherwise, Nginx will continue to use the old version.

Do note, I’ve only been using the above Cap tasks for a couple of weeks. They do the job intended but could definitely use some love.

I’d like to:

  • move the Cap tasks to a gem
  • create a Cap task that generates the app specific nginx.conf
  • make the Cap tasks work w/ the normal Cap workflow, i.e. deploy:cold, etc

@glasner

Comments

On May 22, 2008 at 01:33 mlangenberg says:

You really don’t need to override the migrate task, it’s enough to add the following: set :migrate_env, “MERB_ENV=production”

On May 22, 2008 at 07:01 glasner says:

Thanks for the tip; I just updated the tutorial. Much simpler!

On May 28, 2008 at 07:04 mtodd says:

Well, might need to override the migrate task if using something like Sequel which provides a sequel:db:migrate task instead of db:migrate to run migrations.

On June 03, 2008 at 19:49 hipertracker says:

There is no need for these ugly rules for images, javascripts, etc. This can be solved in more general way:

# serve all static files if (-f $request_filename) { access_log off expires 30d; break; } # server index files if (-f $request_filename/index.html) { rewrite (.*) $1/index.html break; } # server cached files if (-f $request_filename.html) { rewrite (.*) $1.html break; } # all other request send to proxy if (!-f $request_filename) { proxy_pass http://app_name; break; }

BTW, can anybody add preview button for comments?

On June 03, 2008 at 19:51 hipertracker says:

Again:


                # serve all static files
                if (-f $request_filename) {
                   access_log off
                   expires 30d;
                   break;
                 }
                
                # server index files
                if (-f $request_filename/index.html) {
                  rewrite (.*) $1/index.html break;
                }
                
                # server cached files
                if (-f $request_filename.html) {
                 rewrite (.*) $1.html break;
                }     
                
                # all other request send to proxy
                if (!-f $request_filename) {
                 proxy_pass http://app_name;
                 break;
                }
                

Sign in to make your voice heard