Ubuntu + Nginx + Unicorn + Capistrano

Ryan의 Railscasts.com에는,  바로 실무에 적용할 수 있는 정도의 주옥같은 내용들이 자세하게 소개되어 있습니다. 그 중에서도  #335 Deploying to a VPS의 내용을 보면서 가상 서버에 Nginx + Unicorn 조합을 이용하여 Capistrano를 이용하여 배포하는 과정을 시도해 보았습니다.

제가 오늘 작업하는 환경은 다음과 같습니다.

MacBook Air : 13-inch, Mid 2011

  • 프로세서 : 1.8GHz Intel Core i7
  • 메모리 : 4GB 1333 MHz DDR3
  • 그래픽 : Intel HD Graphics 3000 384 MB
  • 운영체제 : Mac OS X Lion 10.7.3(11D50b)

가상서버로는 MacBook Air에서 설치되어 있는 VMware Fusion Version 4.1.1 (536016)의 Guest Server로 Ubuntu 64-bit Server 11.10 버전을 설치하여 사용하였습니다.

우분투 서버를 설치할 때 openSSH-Server만 설치하고 로컬머신에서 SSH로 연결하여 필요한 프로그램들을 설치했습니다.

1. 시스템 업데이트후 Git 설치하기

$ sudo apt-get -y update
$ sudo apt-get -y install curl git-core python-software-properties

2. 웹서버(Nginx) 설치하기

# nginx
$ sudo add-apt-repository ppa:nginx/stable
$ sudo apt-get -y update
$ sudo apt-get -y install nginx
$ sudo service nginx start

3. 데이터베이스(MySQL) 설치하기

# MySQL (instead of PostgreSQL)
$ sudo apt-get -y install mysql-server mysql-client libmysqlclient-dev
$ mysql -u root -p
# create database blog_production;
# grant all on blog_production.* to blog@localhost identified by 'secret';
# exit

4. 서버용 자바스크립트 엔진(Nodejs) 설치하기

# Node.js
$ sudo add-apt-repository ppa:chris-lea/node.js
$ sudo apt-get -y update
$ sudo apt-get -y install nodejs

5. “deployer” 사용자계정 생성후 “admin” 그룹에 추가하기

# Add deployer user
$ sudo adduser deployer --ingroup admin
$ su deployer
$ cd

6. 루비환경관리자(rbenv) 설치하기

# rbenv
$ curl -L https://raw.github.com/fesplugas/rbenv-installer/master/bin/rbenv-installer | bash
$ vim ~/.bashrc # add rbenv to the top
$ . ~/.bashrc
$ rbenv bootstrap-ubuntu-11-10
$ rbenv install 1.9.3-p125
$ rbenv global 1.9.3-p125
$ gem install bundler --no-ri --no-rdoc
$ rbenv rehash

지금까지는 (로컬머신에서 SSH로 접속하여) 가상서버에서 작업을 했습니다. 이제부터는 로컬머신에서 작업할 내용입니다.

7. 로컬머신에서 작업하기

서버에 설치한 것과 동일한 루비버전을 이용하여 “blog”라는 어플리케이션을 생성합니다. 이 때 데이터베이스로는 MySQL을 사용합니다.

$ rails new blog -d mysql
$ cd blog

scaffold generator를 이용하여 Post 리소스를 생성하고 마이그레이션 합니다.

$ rails g scaffold Post title:string content:text
$ rake db:create
$ rake db:migrate

public/index.html을 삭제하고 config/routes.rb 파일에서 루트경로를 posts 컨트롤러의 index 액션으로 정의합니다.

$ rm public/index.html
$ vi config/routes.rb

root :to => “posts#index”

Gemfile에 두개의 젬을 추가해 줍니다(디폴트로 comment 처리되어 있는 unicorn과 capistrano 젬을 uncomment 해 줍니다).

gem "unicorn"
gem "capistrano"

그리고나서, bundle install 합니다.

$ bundle install

capistrano 레시피를 작성합니다. capify 명령을 실행하면 Capfile과 config/deploy.rb 파일 두개가 생성됩니다.

$ capify .

asset pipeline을 사용할 것이기 때문에 Capfile에서 load ‘deploy/assets’을 uncomment 처리해 줍니다.

load 'deploy'
# uncomment if you are using Rails' asset pipeline
load 'deploy/assets'
Dir['vendor/gems/*/recipes/*.rb', 'vendor/plugins/*/recipes/*.rb'].each { |plugin\ load(plugin) }
load 'config/deploy'  # remove this line to skip loading any of the default tasks

config/deploy.rb를 다음과 같이 작성해 줍니다.

require "bundler/capistrano"

set :application, "blog"

# Setup for SCM(Git)
set :scm, :git
set :repository, "git@github.com:your_account/blog.git"
set :branch, "master"

role :web, "192.168.222.130"     # Your HTTP server, Apache/etc
role :app, "192.168.222.130"     # This may be the same as your `Web` server
role :db,  "192.168.222.130", :primary => true # This is where Rails migrations will run

set :user, "deployer"
set :deploy_to, "/home/#{user}/apps/#{application}"
set :deploy_via, :remote_cache
set :use_sudo, false

default_run_options[:pty] = true
ssh_options[:forward_agent] = true

after "deploy", "deploy:cleanup"

namespace :deploy do
  %w[start stop restart].each do |command|
    desc "#{command} unicorn server"
    task command, roles: :app, except: {no_release: true} do
      run "/etc/init.d/unicorn_#{application} #{command}"
    end
  end

  task :setup_config, roles: :app do
    sudo "ln -nfs #{current_path}/config/nginx.conf /etc/nginx/sites-enabled/#{application}"
    sudo "ln -nfs #{current_path}/config/unicorn_init.sh /etc/init.d/unicorn_#{application}"
    run "mkdir -p #{shared_path}/config"
    put File.read("config/database.example.yml"), "#{shared_path}/config/database.yml"
    puts "Now edit the config files in #{shared_path}."
  end
  after "deploy:setup", "deploy:setup_config"

  task :symlink_config, roles: :app do
    run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml"
  end
  after "deploy:finalize_update", "deploy:symlink_config"

  desc "Make sure local git is in sync with remote."
  task :check_revision, roles: :web do
    unless `git rev-parse HEAD` == `git rev-parse origin/master`
      puts "WARNING: HEAD is not the same as origin/master"
      puts "Run `git push` to sync changes."
      exit
    end
  end
  before "deploy", "deploy:check_revision"
end

config/database.yml을 config/database.example.yml로 복사하고 .gitignore 파일에 /config/database.yml을 추가합니다.

$ cp config/database.yml config/database.example.yml

/config/database.yml

git을 초기화한 후 지금까지 작업한 내용을 commit한 후 git 서버로 push합니다.(이 작업 전에 github.com에 blog 저장소를 미리 생성해 두어야 합니다)

$ git init
$ git add .
$ git commit -m "initial commit"
$ git remote add origin git@github.com:your_account/blog.git
$ git push origin master

config/nginx.conf 파일을 생성하고 아래와 같이 작성해 줍니다.

upstream unicorn {
  server unix:/tmp/unicorn.blog.sock fail_timeout=0;
}

server {
  listen 80 default deferred;
  # server_name example.com;
  root /home/deployer/apps/blog/current/public;

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  try_files $uri/index.html $uri @unicorn;
  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://unicorn;
  }

  error_page 500 502 503 504 /500.html;
  client_max_body_size 4G;
  keepalive_timeout 10;
}

config/unicorn.rb 파일을 생성하고 아래와 같이 작성해 줍니다.

root = "/home/deployer/apps/blog/current"
working_directory root
pid "#{root}/tmp/pids/unicorn.pid"
stderr_path "#{root}/log/unicorn.log"
stdout_path "#{root}/log/unicorn.log"

listen "/tmp/unicorn.blog.sock"
worker_processes 2
timeout 30

config/unicorn_init.sh 파일을 생성하고 다음과 같이 작성해 줍니다.

#!/bin/sh
### BEGIN INIT INFO
# Provides:          unicorn
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Manage unicorn server
# Description:       Start, stop, restart unicorn server for a specific application.
### END INIT INFO
set -e

# Feel free to change any of the following variables for your app:
TIMEOUT=${TIMEOUT-60}
APP_ROOT=/home/deployer/apps/blog/current
PID=$APP_ROOT/tmp/pids/unicorn.pid
CMD="cd $APP_ROOT; bundle exec unicorn -D -c $APP_ROOT/config/unicorn.rb -E production"
AS_USER=deployer
set -u

OLD_PIN="$PID.oldbin"

sig () {
  test -s "$PID" && kill -$1 `cat $PID`
}

oldsig () {
  test -s $OLD_PIN && kill -$1 `cat $OLD_PIN`
}

run () {
  if [ "$(id -un)" = "$AS_USER" ]; then
    eval $1
  else
    su -c "$1" - $AS_USER
  fi
}

case "$1" in
start)
  sig 0 && echo >&2 "Already running" && exit 0
  run "$CMD"
  ;;
stop)
  sig QUIT && exit 0
  echo >&2 "Not running"
  ;;
force-stop)
  sig TERM && exit 0
  echo >&2 "Not running"
  ;;
restart|reload)
  sig HUP && echo reloaded OK && exit 0
  echo >&2 "Couldn't reload, starting '$CMD' instead"
  run "$CMD"
  ;;
upgrade)
  if sig USR2 && sleep 2 && sig 0 && oldsig QUIT
  then
    n=$TIMEOUT
    while test -s $OLD_PIN && test $n -ge 0
    do
      printf '.' && sleep 1 && n=$(( $n - 1 ))
    done
    echo

    if test $n -lt 0 && test -s $OLD_PIN
    then
      echo >&2 "$OLD_PIN still exists after $TIMEOUT seconds"
      exit 1
    fi
    exit 0
  fi
  echo >&2 "Couldn't upgrade, starting '$CMD' instead"
  run "$CMD"
  ;;
reopen-logs)
  sig USR1
  ;;
*)
  echo >&2 "Usage: $0 "
  exit 1
  ;;
esac

이 unicorn_init.sh 파일의 권한을 다음과 같이 600으로 변경해 줍니다.

$ chmod +x config/unicorn_init.sh

이제 git commit후 push합니다.

$ git add .
$ git commit -m "deployment configs"
$ git push

이제 cap 명령을 이용하여 서버로 초기화후 배포작업(deploy:cold)을 합니다.

$ cap deploy:setup
# edit /home/deployer/apps/blog/shared/config/database.yml on server
$ cap deploy:check
$ cap deploy:cold

production:
adapter: mysql2
encoding: utf8
reconnect: false
database: blog_production
pool: 5
username: blog
password: secret
socket: /var/run/mysqld/mysqld.sock

일차 배포작업이 에러없이 끝나면 아래와 같이 추가작업을 하여 마무리 합니다.

# after deploy:cold
sudo rm /etc/nginx/sites-enabled/default
sudo service nginx restart
sudo update-rc.d -f unicorn_blog defaults

지금까지 배포작업 중에 에러가 발생하지 않았다면 브라우저에서 서버로 접속할 때 Posts 리소스의 Index 페이지가 보여야 합니다.

소스변경 작업후에 재배포시에는 다음과 같이 한줄이면 끝입니다.

$ cap deploy

긴 여정이었지만, 서버 설치부터 웹작성 및 배포까지 정리해 보았습니다. 기존의 아파치 + Passenger 조합을 이용한 배포에서 Nginx + Unicorn 조합으로 레일스 어플리케이션을 작성하여 배포하는 과정을 정리해 보았습니다. 벤치마킹에서 언급된 바와 같이 Nigix + Unicorn조합의 서버환경에서 요청에 대한 반응속도는 매우 빠르다는 느낌이 팍 와 닿는군요.

그냥 말로만 듣던 Nginx + Unicorn 조합을 찬찬히 따라 해보니까 그렇게 두러워만 할 것이 아니다는 것을 새삼느끼게 되었습니다.

레일스를 시작하시는 분들도 가상 서버를 준비하여 서버 세팅을 조금씩 공부해 두면 나중에 다 도움이 될 것이라는 생각을 해 봅니다.

긴 글을 읽어 주셔서 감사합니다.