Ga Tech

1.01 everyday

Rails Deploy 基礎教學

Using nginx, Unicorn, MySQL, rbenv and Capistrano.

設置主機環境

這邊的主機是以 linode 的 Ubuntu 12.04 LTS 64bit 作示範。

一開始先用 ssh 連到主機:

$ ssh root@xxx.xxx.xxx.xxx  # xxx 為主機的 ip

進去之後,目前的身份會是 root

接著將系統的套件升級:

root@li460-213:~# apt-get update

然後安裝必要套件:

root@li460-213:~# apt-get -y install curl git-core python-software-properties

安裝 nginx

再來要安裝 nginx,我們可以用 apt-get 的方式來安裝 nginx,但是這樣版本會過舊。
為了避免版本過舊,我們可以新增一個新的 repository(版本會是比較近期的):

root@li460-213:~# add-apt-repository ppa:nginx/stable

接著再輸入以下指令,就可以使用 apt-get 的方式來安裝 nginx:

root@li460-213:~# apt-get update
root@li460-213:~# apt-get -y install nginx

安裝完畢之後,就可以啓動 nginx 了:

root@li460-213:~# service nginx start
Starting nginx: nginx.

我們可以在瀏覽器輸入主機的 ip,這時候可以看到畫面顯示:Welcome to nginx!,就表示成功啓動 nginx 了。
(如果有出現 nginx: [emerg] bind() to [::]:80 failed (98: Address already in use) 錯誤的話,請看文章最底下 nginx binding issue。)

安裝 MySQL

輸入以下指令來安裝 MySQL:

root@li460-213:~# apt-get install -y mysql-server libmysqlclient-dev

過程中會要求輸入身份為 root 的 MySQL 密碼。

安裝完成之後,要進入 mySQL 設定個別權限:

root@li460-213:~# mysql -u root -p
Enter password:

接著針對我們的 Rails application 建立 demo user以及 database:

mysql> create user demo@localhost identified by 'secret';
Query OK, 0 rows affected (0.01 sec)
mysql> create database demo_production;
Query OK, 0 rows affected (0.01 sec)
mysql> grant all privileges on demo_production.* to demo@localhost
Query OK, 0 rows affected (0.01 sec)
mysql> exit

安裝 Postfix and Node.js

如果要讓 application 能夠寄信,可以安裝 Postfix:

root@li460-213:~# apt-get install -y postfix

過程中選擇預設的 “Internet Site” 以及預設的 “System mail name” 即可。

接著安裝 node.js。這樣一來,主機就可以執行 Javascript 來協助處理 application 中的 asset pipeline。

root@li460-213:~# add-apt-repository ppa:chris-lea/node.js
root@li460-213:~# apt-get update
root@li460-213:~# apt-get -y install nodejs

安裝 Ruby

在安裝 Ruby 之前,我們先建立一個新的 user 叫做 deployer(先前都是以 root 的身份執行)。
首先建立一個 group 名為 admin,接著把 root 拉進去:

root@li460-213:~# groupadd admin
root@li460-213:~# usermod -g admin root

然後新增 deployer user,並將其加到 admin group 裡面,這樣一來,deployer 就有了 sudo 的權限:

root@li460-213:~# adduser deployer --ingroup admin

過程中會要你輸入 deployer 的密碼,其他設定則是保留預設值。
接著把身份換到 deployer,並跳到 home 資料夾:

root@li460-213:~# su deployer
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

deployer@li460-213:/root$ cd ~

這邊使用 rbenv-installer 來安裝 ruby,手續會比較簡便。

deployer@li460-213:~$ curl -L https://raw.github.com/fesplugas/rbenv-installer/master/bin/rbenv-installer | bash

安裝過程最後會出現一段 code(code 可能會因主機不同而有差異),要把這段加到 .bashrc 檔案當中:

export RBENV_ROOT="${HOME}/.rbenv"

if [ -d "${RBENV_ROOT}" ]; then
  export PATH="${RBENV_ROOT}/bin:${PATH}"
  eval "$(rbenv init -)"
fi

接下來用 Vim 打開 .bashrc 檔:

deployer@li460-213:~$ vim ~/.bashrc

然後找到其中一段:

~/.bashrc
1
2
# If not running interactively, don't do anything
[ -z "$PS1" ] && return

這段的意思是如果不在 interactive shell 狀態就停止執行,因此我們要在這之前就先讀進 rbenv 才可以使用 Capistano。

把剛剛 rbenv-installer 安裝程序最後出現的 code 加到這段上面:

~/.bashrc
1
2
3
4
5
6
7
8
9
export RBENV_ROOT="${HOME}/.rbenv"
if [ -d "${RBENV_ROOT}" ]; then
export PATH="${RBENV_ROOT}/bin:${PATH}"
eval "$(rbenv init -)"
fi
# If not running interactively, don't do anything
[ -z "$PS1" ] && return

.bashrc 存檔了之後,要再 reload 一次:

deployer@li460-213:~$ . ~/.bashrc

這樣一來,就可以使用 rbenv command 了。我們接著輸入以下指令,它會安裝 Ruby 相依的 packages:

deployer@li460-213:~$ rbenv bootstrap-ubuntu-12-04

再來根據 rbenv-installer 安裝最新版的 Ruby:

deployer@li460-213:~$ rbenv install 1.9.3-p374

經過大概一碗泡麵的時間就可以安裝完成。接著將其設定為預設的 Ruby 版本:

deployer@li460-213:~$ rbenv global 1.9.3-p374
deployer@li460-213:~$ ruby -v
ruby 1.9.3p374 (2013-01-15 revision 38858) [x86_64-linux]

最後要安裝 Bundler,然後執行 rbenv rehash 就可以使用 bundle command 了:

deployer@li460-213:~$ gem install bundler --no-ri --no-rdoc
deployer@li460-213:~$ rbenv rehash
deployer@li460-213:~$ bundle -v
Bundler version 1.2.4

Application 設定

現在回到我們的電腦上面,這邊假設我們的 application 已經有對應到 Github 上面的 repository,僅針對部分需要注意的地方稍微提一下。

ignore database.yml

我們會將 database 的帳號密碼放在 database.yml 底下,為了避免在 Github 上面公開,因此應該把 database.yml 加到 .gitignore 當中,並且在主機上面手動設定 database.yml 檔。

/.gitignore
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# See http://help.github.com/ignore-files/ for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile ~/.gitignore_global
# Ignore bundler config
/.bundle
# Ignore the default SQLite database.
/db/*.sqlite3
# Ignore all logfiles and tempfiles.
/log/*.log
/tmp
/config/database.yml

為了方便在主機上面手動設定 database.yml,我們可以先複製一份範本,之後再拿來改:

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

Deploy with Capistrano

這邊會使用 Capistrano 來進行 deploy。此外還會用到 Unicorn,因此我們要在 Gemfile 加上這兩個 gem:

1
2
3
4
5
# Use unicorn as the app server
gem 'unicorn'
# Deploy with Capistrano
gem 'capistrano'

記得要再執行 bundle 來安裝。

接著執行 capify . 來設定 Capistrano,這個指令會產生兩個設定檔:

$ capify .
[add] writing './Capfile'
[add] writing './config/deploy.rb'
[done] capified!

首先看到 Capfile 檔,第二行告訴我們如果使用 asset pipeline 的話就要反註解,而我們也的確會用到:

/Capfile
1
2
3
4
load 'deploy'
# Uncomment if you are using Rails' asset pipeline
load 'deploy/assets'
load 'config/deploy' # remove this line to skip loading any of the default tasks

接下來看到 deploy.rb 檔,當中會有許多個人化的設定,因此這邊只會貼出幾項必要設定:

/config/deploy.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
require "bundler/capistrano"
server "xxx.xxx.xxx.xxx", :web, :app, :db, primary: true
set :application, "demo"
set :user, "deployer"
set :deploy_to, "/home/#{user}/apps/#{application}"
set :deploy_via, :remote_cache
set :use_sudo, false
set :scm, "git"
set :repository, "git@github.com:GGD/#{application}.git"
set :branch, "master"
default_run_options[:pty] = true
ssh_options[:forward_agent] = true
after "deploy", "deploy:cleanup" # keep only the last 5 releases
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

Setup Nginx/Unicorn in Rails application

Nginx

接下來要針對我們的 Rails application 進行 Nginx 設定,首先在 application 底下新增 /config/nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
upstream unicorn {
server unix:/tmp/unicorn.demo.sock fail_timeout=0;
}
server {
listen 80 default deferred;
# server_name example.com;
root /home/deployer/apps/demo/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;
}

其中 server block 與 Apache 的 VirtualHost 類似。
第 1 ~ 3 行的 upstream block 告訴 nginx 要將動態網頁導到 Unicorn,這邊給定 upstream block 名為 unicorn,並使用 server 指向 Unix socket (對應底下的 Unicorn 設定)。
同時會將 fail_timeout 設定為 0,這樣如果 Rails application 無法回應且 Unicorn 已經 times out 時,nginx 才能夠正確處理。

第 6 行告訴 server 要 listen port 80,並將其設定為 default,表示如果找不到 matching server name 的話就使用這個 server (可參考這篇)。deferred 則是會啓動 Ubuntu 的 TCP_DEFER_ACCEPT 設定 (可參考這篇)。

第 7 行是如果有 domain name 的話,可以在此輸入。

第 8 行的 root 則是告訴 nginx 這個 application 的 static files 在哪裡。

第 10 行使用了 nginx 的 gzip_static module。在收到 request 後,會到 /assets/ 當中尋找 .gz 的壓縮檔。比如 http://www.example.com/stylesheets/homepage.css,nginx 就會先去找 /assets/stylesheets/homepage.css.gz 這個檔案,如果存在的話就直接回傳;如果不存在,就將 /assets/stylesheets/homepage.css 進行 gzip 壓縮之後再回傳,這樣就可以避免重複的壓縮動作。這個 module 會對所有 request 都有效,而一般來說,大部份的 request 都是屬於 dynamic 的,並不會有 .gz,所以建議將這個 module 指到含有 .gz 的靜態資料夾 /assets/ 比較恰當。

第 16 行是告訴 nginx 去 try 這些 list 是否存在於剛剛設定的 root 當中,如果不存在的話,就會到 Rails application 去產生。比如找不到 index.html 的話,就會到 Rails application 產生;而 $uri 則是 user 傳過來的 URL。

至於該如何找到 Rails application,就是要透過 named location 了,在這裡我們命名為 @unicorn (對應到第 17 行的 location command)。在 location command 當中,nginx 會透過 proxy_pass 把收到的 request 丟給 Unicorn server 來處理。

第 21 行是告訴 nginx 要 proxy_passhttp://unicorn ,這樣就會指向到上面的 upstream block (記得要設定與上面的 upstream 名稱相同才行)。

第 24 行則是告訴 nginx 如果遇到 500, 502, 503, 504 錯誤的話,就顯示 Rails application 的 500.html 頁面。

Unicorn

接下來要設定 Unicorn,同樣也是在 Rails application 底下新增 /config/unicorn.rb

/config/unicorn.rb
1
2
3
4
5
6
7
8
9
root = "/home/deployer/apps/demo/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.demo.sock"
worker_processes 2
timeout 30

第 1, 2 行設定了 Rails application 的位置。
第 3 ~ 5 行則是設定了 pid 以及 log 的位置。
第 7 行告訴 Unicorn 要 listen 哪個 socket (或是改成 port 也可以,變成傳入數字)。
第 8 行是告訴 Unicorn 在 boot up 的時候,有多少個 Rails instances,最少應該要等於這台主機的 cpu 核心數(關於 worker_precesses 的調校可以看這篇)。
第 9 行則是設定了 timeout 的時間,這邊是 30 秒。

最後,我們會使用 shell script 來啓動 Unicorn,方法一樣是新增一個檔案 /config/unicorn_init.sh,雖然這個檔案有很多設定,但其實我們只要動到最上面的設定就好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#!/bin/sh
set -e
# Feel free to change any of the following variables for your app:
TIMEOUT=${TIMEOUT-60}
APP_ROOT=/home/deployer/apps/demo/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

然後要將這段 script 的權限設定為 executable:

$ chmod +x config/unicorn_init.sh

記得要把這些設定加到 Git 裡:

$ git add .
$ git commit -m 'Deployment config files'
$ git push origin master

Deploy

現在, 可以執行 cap deploy:setup 來 deploy 我們的 Rails application 了 (有些人可能會因為 shell 設定不同,需要在指令前面加上 bundle exec),這個指令會以 deployer 的身份登入到 server。

$ cap deploy:setup

這段指令會到 server 新增一些與 application 有關的資料夾,然後根據前面的設定,將 Nginx 與 Unicorn 設定到 server,再上傳 database.yml。我們接著要登入到 sever 來修改 database.yml

$ ssh deployer@xxx.xxx.xxx.xxx

登入之後就進行修改:

deployer@li460-213:~$ cd apps/demo/shared/config/
deployer@li460-213:~/apps/demo/shared/config$ vim database.yml

我們只需要 production 環境設定,因此只要留下 production 部分就好:

/apps/demo/shared/config/database.yml
1
2
3
4
5
6
7
8
production:
adapter: mysql2
database: demo_production
pool: 5
username: demo
password: demo
host: localhost
encoding: utf8

這是我們唯一需要登入到 server 修改的檔案,所以在修改完了之後就可以登出了。

自動登入 Server

每次使用 ssh 登入到 server 都需要輸入密碼是一件很麻煩的事,我們可以輸入以下指令來免除這個麻煩。這個指令會複製 public RSA 到 deployers authorized keys 當中。

$ cat ~/.ssh/id_rsa.pub | ssh deployer@xxx.xxx.xxx.xxx 'cat >> ~/.ssh/authorized_keys'

另外我們還需要執行 ssh-add 讓 SSH Agent 能夠作用。在 Capistrano deployment 檔案當中有一行:

/config/deploy.rb
1
ssh_options[:forward_agent] = true

這行告訴 Capistrano 使用 local keys 而非 server keys。這行搭配 ssh-add 表示當我們 ssh 到 server 的時候,可以讓 server 存取 Github,並且無需再針對 server 設定一組 deploy key 到 Github 裡。

如果是 Mac OSX 的話,我們還可以加上 -K 參數,這樣就可以把 passphrase 加到 keychain 裡。

$ssh-add -K

接著就可以 deploy 我們的 application 了:

$ cap deploy:cold

cold deploy 表示要執行 database migrations 並且讓 server 保持在 start 狀態 (而非 restart)。
用 cold deploy 的原因在於,有時候第一次執行時會噴一堆錯誤,此時就可以看一下錯誤資訊趕快做修正。

Configuring Nginx

此時如果用 browser 拜訪我們的網站,會發現仍然是 “Welcome to nginx” 頁面,這是因為 nginx 預設的頁面還留著的緣故。所以我們要再次 ssh 到 server 裡,然後把預設的頁面拿掉並 restart nginx:

deployer@li460-213:~$ sudo rm /etc/nginx/sites-enabled/default
[sudo] password for deployer:
deployer@li460-213:~$ sudo service nginx restart
Restarting nginx: nginx.

第一行指令是拿掉 symbolic link,這樣可以確保我們不會誤刪掉重要的檔案。

同樣是在 server 當中,我們還要執行一段指令,讓 Unicorn 在 server restart 時候可以正確地啟動:

deployer@li460-213:~$ sudo update-rc.d unicorn_demo defaults

這段指令當中,把我們的 service 名稱傳進去,這邊是 unicorn_demo,後面再接著 defaults

再次用 browser 拜訪網站,就可以看到我們的 Rails application 了。

後續如果 application 有修改,只要將修改的部分 push 到 Github,然後執行 cap deploy 就會自動 deploy 了!

$ git commit -am 'Fixing something'
$ git push
$ cap deploy

Trouble Shooting

nginx binding issue

/etc/nginx/sites-enabled/ 資料夾底下有個 default 檔案,將其中一行:
listen [::]:80 default_server;
給註解掉,再執行 service nginx restart 就可以了。

Comments