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 조합을 찬찬히 따라 해보니까 그렇게 두러워만 할 것이 아니다는 것을 새삼느끼게 되었습니다.

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

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

글쓴이: 최효성

외과전문의,웹프로그래밍,컴퓨터 일러스트레이션 / Surgeon, Medical Illustration, Web Programmer

6 thoughts on “Ubuntu + Nginx + Unicorn + Capistrano”

  1. 안녕하세요. 블로그 너무 좋으네요 ^^
    참고 잘하고 있습니다. 그런데 제가 처음 입문자이다 보니 모르는 것이 많은데요.
    현재는 개발만 약간해본 상태입니다.
    서버 구성이 … 운영서버, 개발머신, 배포서버 이렇게 구성하는 것인가요?
    로컬머신이라는 것은 개발머신을 말하는 것인지 잘 모르겠어요ㅜ배포를 해볼까 하는데 너무 어렵네요..댓글 주시면 감사하겠습니다.

    1. 답변이 늦었습니다. 로컬머신이라는 것은 개발장비를 말하죠. 즉, 맥북… 운영서버는 실제로 웹서비스를 운영하는 서버죠. 배포서버라는 말은 실제 사용자들에게 웹서비스를 하게되는 서버, 즉, 운영서버를 말합니다. 이 글에서 문맥상 이해하시면 됩니다. 감사합니다.

      1. 답변 감사합니다 ^^
        여기저기 블로그를 참고하여 성공했습니다.
        설 명절 잘 보내시고, 복 많이 받으세요 ^^

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중