Ubuntu + Nginx + Unicorn + Capistrano

RyanRailscasts.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 처리되어 있는 unicorncapistrano 젬을 uncomment 해 줍니다).

gem "unicorn" 
gem "capistrano"

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

$ bundle install 

capistrano 레시피를 작성합니다. capify 명령을 실행하면 Capfileconfig/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.ymlconfig/database.example.yml로 복사하고 .gitignore 파일에 /config/database.yml을 추가합니다.

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

/config/database.yml

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

$ 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 <start|stop|restart|upgrade|force-stop|reopen-logs>"
  exit 1
  ;;
esac 

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

$ chmod +x config/unicorn_init.sh 

이제 git commitpush합니다.

$ 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 

긴 여정이었지만, 서버 설치부터 웹작성 및 배포까지 정리해 보았습니다.

기존의 Apache + Passenger 조합을 이용한 배포에서 Nginx + Unicorn 조합으로 레일스 어플리케이션을 작성하여 배포하는 과정을 정리해 보았습니다.

벤치마킹에서 언급된 바와 같이 Nigix + Unicorn 조합의 서버환경에서 요청에 대한 반응속도는 매우 빠르다는 느낌이 팍 와 닿는군요.

그냥 말로만 듣던 Nginx + Unicorn 조합을 찬찬히 따라 해보니까 그렇게 두러워만 할 것이 아니다는 것을 새삼느끼게 되었습니다. 레일스를 시작하시는 분들도 가상 서버를 준비하여 서버 세팅을 조금씩 공부해 두면 나중에 다 도움이 될 것이라는 생각을 해 봅니다.

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

6 Comments

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

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

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

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.