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 <start|stop|restart|upgrade|force-stop|reopen-logs>"
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
긴 여정이었지만, 서버 설치부터 웹작성 및 배포까지 정리해 보았습니다.
기존의 Apache + Passenger 조합을 이용한 배포에서 Nginx + Unicorn 조합으로 레일스 어플리케이션을 작성하여 배포하는 과정을 정리해 보았습니다.
벤치마킹에서 언급된 바와 같이 Nigix + Unicorn 조합의 서버환경에서 요청에 대한 반응속도는 매우 빠르다는 느낌이 팍 와 닿는군요.
그냥 말로만 듣던 Nginx + Unicorn 조합을 찬찬히 따라 해보니까 그렇게 두러워만 할 것이 아니다는 것을 새삼느끼게 되었습니다. 레일스를 시작하시는 분들도 가상 서버를 준비하여 서버 세팅을 조금씩 공부해 두면 나중에 다 도움이 될 것이라는 생각을 해 봅니다.
긴 글을 읽어 주셔서 감사합니다.
bundle이 없다고 에러메세지 뜨는 문제가 있었는데 옆 블로그 보고 해결했어요. http://www.ruby-forum.com/topic/4204446
Hi my friend! I wish to say that this post is amazing, nice written and come with
approximately all significant infos. I’d like to see more posts like this .
안녕하세요. 블로그 너무 좋으네요 ^^
참고 잘하고 있습니다. 그런데 제가 처음 입문자이다 보니 모르는 것이 많은데요.
현재는 개발만 약간해본 상태입니다.
서버 구성이 … 운영서버, 개발머신, 배포서버 이렇게 구성하는 것인가요?
로컬머신이라는 것은 개발머신을 말하는 것인지 잘 모르겠어요ㅜ배포를 해볼까 하는데 너무 어렵네요..댓글 주시면 감사하겠습니다.
답변이 늦었습니다. 로컬머신이라는 것은 개발장비를 말하죠. 즉, 맥북… 운영서버는 실제로 웹서비스를 운영하는 서버죠. 배포서버라는 말은 실제 사용자들에게 웹서비스를 하게되는 서버, 즉, 운영서버를 말합니다. 이 글에서 문맥상 이해하시면 됩니다. 감사합니다.
답변 감사합니다 ^^
여기저기 블로그를 참고하여 성공했습니다.
설 명절 잘 보내시고, 복 많이 받으세요 ^^
감사합니다. 수고하셨습니다. ^^