【Rails】使用 Capistrano3 + Unicorn + Nginx 部署服务器

背景

本文简单叙述了 Unicorn + Nginx 部署 Rails 项目的设定。
相较于传统的 Apache + Passenger,Unicorn + Nginx 的组合以其高效率而获得越来越多人的青睐。

导入nginx

安装

1
$ sudo yum install nginx

设定

主要设定文件

/etc/nginx/global.conf:

1
2
3
4
5
6
7
8
9
10
11
12
user www-data;
pid /var/run/nginx.pid;
error_log /var/log/nginx/error.log;
worker_processes auto;
events {
worker_connections 2048;
use epoll;
multi_accept on;
}

/etc/nginx/nginx.conf:

1
2
3
4
5
6
7
8
9
include /etc/nginx/global.conf;
http {
server_tokens off;
access_log /var/log/nginx/access.log combined;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

conf.d/

/etc/nginx/conf.d/format.combined2.conf:

1
2
3
4
# $request_time prefixed version of `combined` format
log_format combined2 '$request_time $remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';

/etc/nginx/conf.d/format.ltsv.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
log_format ltsv 'reqtime:$request_time\t'
'host:$remote_addr\t'
'user:$remote_user\t'
'time:$time_local\t'
'msec:$msec\t'
'method:$request_method\t'
'uri:$request_uri\t'
'status:$status\t'
'size:$body_bytes_sent\t'
'referer:$http_referer\t'
'ua:$http_user_agent\t'
'apptime:$upstream_response_time\t'
'runtime:$upstream_http_x_runtime';

/etc/nginx/conf.d/mimetype.conf:

1
2
include /etc/nginx/mime.types;
default_type application/octet-stream;

/etc/nginx/conf.d/network.conf:

1
2
3
4
5
sendfile on;
tcp_nodelay on;
tcp_nopush on;
keepalive_timeout 1;

/etc/nginx/conf.d/nginx_stub.conf:

1
2
3
4
5
6
7
8
9
10
11
server {
listen 80;
server_name localhost;
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
}

/etc/nginx/conf.d/open_file_cache.conf:

1
2
3
4
open_file_cache max=100000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;

/etc/nginx/conf.d/realip.conf:

1
2
3
# real ip module(for reverse proxy structure)
set_real_ip_from 10.164.0.0/16;
real_ip_header X-Cluster-Client-Ip;

/etc/nginx/sites-enabled/

example:

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# statements for each of your virtual hosts
upstream static_error {
server localhost:8082;
}
server {
listen 8082;
server_name example.com www.exmaple.com xxx.xx.xxx.xxx;
location / {
root /var/www/example/current/public;
}
}
server {
listen 80;
client_body_buffer_size 20M;
client_max_body_size 20M;
server_name example.com www.example.com xxx.xx.xxx.xxx;
gzip on;
gzip_min_length 1100;
gzip_buffers 4 32k;
gzip_types
text/plain
text/css
text/js
text/xml
text/javascript
application/javascript
application/x-javascript
application/json
application/xml
application/xml+rss;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
gzip_disable "Mozilla/4";
access_log /var/log/nginx/example/access.log ltsv;
error_log /var/log/nginx/example/error.log debug;
large_client_header_buffers 4 32k;
location / {
root /var/www/example/current/public;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
if ($http_user_agent ~* "NewRelicPinger") {
return 200;
}
if (-f $request_filename) {
access_log off;
expires 7d;
break;
}
error_page 500 502 504 /500.html;
error_page 503 @503;
# Return a 503 error if the maintenance page exists.
if (-f /var/www/example/shared/public/system/maintenance.html) {
return 503;
}
# maintenance mode start
# set $maintenance_rule '';
# if ( $uri !~ "system/maintenance\.json" ){
# set $maintenance_rule "U${maintenance_rule}";
# }
# if ( -e /tmp/.nginx_maintenance ){
# set $maintenance_rule "E${maintenance_rule}";
# }
# if ( $maintenance_rule = EU ){
# return 503;
# error_page 503 @maintenance;
# }
# maintenance mode end
# allow 122.212.33.130; # gateway
# allow 192.168.160.0/22;
# allow 192.168.180.0/22;
# allow 192.168.200.0/23;
# deny all;
}
location /favicon.ico {
root /var/www/example/current/public;
}
location /apple-touch-icon.png {
root /var/www/example/current/public;
}
location /apple-touch-icon-precomposed.png {
root /var/www/example/current/public;
}
location @503 {
# Serve static assets if found.
if (-f $request_filename) {
break;
}
# Set root to the shared directory.
root /var/www/example/shared/public;
rewrite ^(.*)$ /system/maintenance.html break;
}
location @maintenance {
root /var/www/example/current/public;
rewrite ^/(.*)$ /system/maintenance.json break;
add_header Cache-Control no-cache;
proxy_method GET;
proxy_pass http://static_error;
}
location ~* ^/uploads/.+/image/{
auth_basic off;
root /var/www/example/current/public;
if (-f $request_filename) {
access_log off;
expires 7d;
break;
}
allow all;
}
location ^~ /(admin|manage) {
send_timeout 300;
if (-f $request_filename) {
access_log off;
expires 1h;
break;
}
root /var/www/example/current/public;
allow all;
}
}

导入 Unicorn

安装

Gemfile:

1
gem 'unicorn'

1
$ bundle install

设定

添加 config/unicorn.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
config/unicorn.rb
# -*- coding: utf-8 -*-
worker_processes Integer(ENV["WEB_CONCURRENCY"] || 3)
timeout 15
preload_app true # 更新時ダウンタイム無し
listen "/tmp/unicorn.sock"
pid "/tmp/unicorn.pid"
before_fork do |server, worker|
Signal.trap 'TERM' do
puts 'Unicorn master intercepting TERM and sending myself QUIT instead'
Process.kill 'QUIT', Process.pid
end
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
end
after_fork do |server, worker|
Signal.trap 'TERM' do
puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT'
end
defined?(ActiveRecord::Base) and
ActiveRecord::Base.establish_connection
end
# ログの出力
stderr_path File.expand_path('log/unicorn.log', ENV['RAILS_ROOT'])
stdout_path File.expand_path('log/unicorn.log', ENV['RAILS_ROOT'])

导入 Capistrano3

安装

Gemfile:

1
2
3
4
5
6
group :deployment do
gem 'capistrano'
gem 'capistrano_colors'
gem 'capistrano-ext'
gem 'capistrano_rsync_with_remote_cache'
end

1
2
$ bundle install
$ bundle exec cap install

部署的设定

由上述命令生成 /Capfile 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
require 'capistrano/setup'
require 'capistrano/deploy'
# rbenvを使用している場合
require 'capistrano/rbenv'
# デプロイ先のサーバで、ユーザディレクトリでrbenvをインストールしている場合
set :rbenv_type, :user
set :rbenv_ruby, '2.1.2'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
require 'capistrano3/unicorn'
# Rails4から分離したsecrets.ymlの環境変数を .envファイルで管理する
set :linked_files, %w{config/secrets.yml .env}
# タスクを読み込むけど、今回は特に使わない
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# config valid only for Capistrano 3.1
lock '3.1.0'
set :application, "exmaple"
set :repo_url, "git@github.com:example/example-server.git"
# Default branch is :master
# ask :branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp }
# Default deploy_to directory is /var/www/my_app
# set :deploy_to, '/var/www/my_app'
set :deploy_to, "/var/www/#{fetch(:application)}"
# Default value for :scm is :git
# set :scm, :git
# Default value for :format is :pretty
# set :format, :pretty
# Default value for :log_level is :debug
# set :log_level, :debug
# Default value for :pty is false
# set :pty, true
# Default value for :linked_files is []
# set :linked_files, %w{config/database.yml}
# Default value for linked_dirs is []
# set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system}
# Default value for default_env is {}
# set :default_env, { path: "/opt/ruby/bin:$PATH" }
# Default value for keep_releases is 5
# set :keep_releases, 5
# ------------------------------------------------------------
# capistrano-bundler
# ------------------------------------------------------------
set :bundle_roles, :bundle
set :bundle_flags, '--deployment --quiet'
set :bundle_path, -> { shared_path.join('vendor/bundle') }
#-----------------------------------------------------------------
# Maintenance
#-----------------------------------------------------------------
set :app_maintenance_file, "/var/tmp/.app_maintenance"
set :nginx_maintenance_file, "/var/tmp/.nginx_maintenance"
# -----------------------------------------------------------
# SSH setting
# -----------------------------------------------------------
set :user, (ENV['CAP_USER'] || ENV['USER'] || "#{`whoami`.chomp}")
set :ssh_options, {
user: fetch(:user),
port: 10022,
forward_agent: true,
}
# -----------------------------------------------------------
# NewRelic
# -----------------------------------------------------------
set :new_relic_app_name, "example"
set :new_relic_monitor_mode, false
# -----------------------------------------------------------
# Unicorn Config
# -----------------------------------------------------------
set :unicorn_worker_processes, 2
set :unicorn_working_directory, current_path
set :unicorn_sock, "/tmp/unicorn.sock"
set :unicorn_backlog, 128
set :unicorn_port, 8080
set :unicorn_timeout, 30
set :unicorn_preload_app, true
set :unicorn_pid_file, File.join(current_path, "tmp", 'pids', 'unicorn.pid')
set :unicorn_stderr_path, File.join(current_path, "log", "unicorn.stderr.log")
set :unicorn_stdout_path, File.join(current_path, "log", "unicorn.stdout.log")
# Default value for :format is :pretty
# set :format, :pretty
# Default value for :log_level is :debug
# set :log_level, :debug
# Default value for :pty is false
set :pty, true
# Default value for :linked_files is []
# set :linked_files, %w{config/database.yml}
SSHKit.config.command_map[:rake] = 'bundle exec rake'
set :filter, roles: %w{app web kvs migrator job bundle}
# Default value for linked_dirs is []
set :linked_dirs, %w{log tmp vendor/bundle public/system public/assets public/uploads}
# Default value for default_env is {}
# set :default_env, { path: "/opt/ruby/bin:$PATH" }
# Default value for keep_releases is 5
set :keep_releases, 5
set :config_path, File.dirname(__FILE__)
set :templates_path, File.join(fetch(:config_path), "deploy", "templates")
namespace :deploy do
desc 'Restart application'
task :restart do
on roles(:app), in: :sequence, wait: 5 do
# Your restart mechanism here, for example:
# execute :touch, release_path.join('tmp/restart.txt')
end
end
after :publishing, :restart
after :restart, :clear_cache do
on roles(:web), in: :groups, limit: 3, wait: 10 do
# Here we can do anything such as:
# within release_path do
# execute :rake, 'cache:clear'
# end
end
end
namespace :before_hook do
desc 'Start a deployment, make sure server(s) ready.'
task :starting do
puts "example:change_permission】"
invoke('example:change_permission')
puts "example:git_setup】"
invoke('example:git_setup')
end
end
before :starting, 'before_hook:starting'
namespace :after_hook do
desc 'Update server(s) by setting up a new release.'
task :updating do
puts "example:setup】"
invoke('example:setup:config')
end
desc 'Finished'
task :finished do
# git:check で作成されるディレクトリを削除
on release_roles :all do
execute :rm, "-rf", "#{fetch(:tmp_dir)}/#{fetch(:application)}"
end
end
end
after :updating, 'after_hook:updating'
after :finishing, 'cleanup'
after :finished, 'after_hook:finished'
end
namespace :example do
namespace :setup do
task :config do
on roles(:app), in: :sequence do
tmpdir_path = File.join(fetch(:tmp_dir), fetch(:user))
execute "mkdir -p #{tmpdir_path}"
Dir.chdir(fetch(:templates_path)) do |path|
Dir.glob("*.erb") do |file|
remote_filename = file.gsub(/.erb$/, "")
remote_path = File.join(release_path, "config", remote_filename)
info "Uploading: #{file} -> #{remote_path}"
file_content = ERB.new(File.read(file))
converted_content = StringIO.new(file_content.result(binding))
tmpfile = File.join(tmpdir_path, remote_filename)
upload! converted_content, tmpfile
execute "mv #{tmpfile} #{remote_path}"
execute "chmod a+r #{remote_path}"
end
end
end
end
end
namespace :nginx do
desc "start nginx process"
task :start do
on roles(:web), in: :sequence do
sudo "/etc/init.d/nginx start"
end
end
desc "restart nginx process"
task :restart do
on roles(:web), in: :sequence do
sudo "/etc/init.d/nginx restart"
end
end
desc "reload nginx configuration"
task :reload do
on roles(:web), in: :sequence do
sudo "/etc/init.d/nginx reload"
end
end
desc "stop nginx process"
task :stop do
on roles(:web), in: :sequence do
sudo "/etc/init.d/nginx stop"
end
end
end
namespace :stage do
desc "input stage validation"
task :validation do
run_locally do
ask(:input_stage, "stage == #{fetch(:stage)}")
if fetch(:input_stage) == fetch(:stage).to_s
puts "valid!"
else
raise "Invalid stage"
end
end
end
end
task :git_setup do
on roles(:app) do
within "#{fetch(:deploy_to)}/repo" do
sudo "chmod -R g+ws *"
sudo "chgrp -R example-users *"
execute :git, "config repo.core.sharedRepository true"
end
end
end
# change_permission
task :change_permission do
on roles(:all) do
within fetch(:deploy_to) do
sudo "chmod -R g+w *"
sudo "chgrp -R example-users *"
end
end
end
namespace :db do
set :db_environmental_variables, ->() {
{
rails_env: fetch(:rails_env),
}
}
desc "rake db:create"
task :create do
on roles(:migrator), in: :sequence do
with rails_env: fetch(:rails_env) do
within current_path do
execute :bundle, :exec, :rake, 'db:create'
end
end
end
end
desc "rake db:seed"
task :seed do
on roles(:migrator), in: :sequence do
within current_path do
env_variables = fetch(:db_environmental_variables).dup
env_variables[:tables] = ENV['TABLES'] if ENV['TABLES']
env_variables[:catalogs] = ENV['CATALOGS'] if ENV['CATALOGS']
with env_variables do
execute :bundle, :exec, :rake, 'db:seed'
end
end
end
end
end
end
namespace :maintenance do
desc "Start nginx maintenance(End command: maintenance:nginx_end)"
task :nginx_start do
on roles(:web) do
execute :touch, fetch(:nginx_maintenance_file)
execute :chmod, '777', fetch(:nginx_maintenance_file)
end
end
desc "End nginx maintenance"
task :nginx_end do
on roles(:web) do
execute :rm, '-f', fetch(:nginx_maintenance_file)
#execute :rm, '-f', File.join(current_path, 'public', 'system', 'maintenance.html')
end
end
desc "Start maintenance"
task :start do
on roles(:web) do
execute :touch, fetch(:app_maintenance_file)
execute :chmod, '777', fetch(:app_maintenance_file)
end
end
desc "End maintenance"
task :end do
on roles(:web) do
execute :rm, '-f', fetch(:app_maintenance_file)
end
end
desc "put maitenance json"
task :json do
on roles(:web) do
remote_system_path = File.join(current_path, 'public', 'system')
# メンテナンスjsonのupload
file_path = File.join('public', 'system', 'maintenance.json')
output_path = File.join(remote_system_path, "maintenance.json")
upload_file(file_path, output_path)
execute :chmod, '-R a+r ', output_path
execute :chmod, '-R g+w ', output_path
end
end
#before 'maintenance:nginx_start', 'maintenance:json'
end

config/deploy/exmaple.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
set :stage, :production
set :rails_env, :production
example_web_01 = "xxx.xx.xxx.xxx"
example_db_01 = "localhost"
example_job_01 = "xxx.xx.xxx.xxx"
example_kvs_01 = "xxx.xx.xxx.xxx"
set :branch, "master"
set :domain_name, ''
set :application_hostname, 'example'
set :new_relic_monitor_mode, true
set :db_path, "/usr/bin/mysql"
set :db_root_user, 'root'
set :db_root_password, '***'
set :db_user, 'example'
set :db_password, 'example'
set :db_host, example_db_01
set :db_name, 'example_production'
set :db_pool, 10
set :redis_data_host, example_kvs_01
set :redis_queue_host, example_kvs_01
server example_web_01, roles: %w{bundle web app migrator job db}, primary: true
invoke "example:stage:validation"

Yunjie Zhang wechat
扫一扫上面的二维码加我微信