CentOS 上使用 Nginx Passenger 部署 Ruby on Rails

本教程将会涉及以下工具:

  • Centos 7
  • [[RVM]]
  • Ruby 2.2.2
  • Rails 4.0.3
  • Passenger 4.0+
  • Nginx(由 Passenger 编译)

创建帐号

假设你已经用 root 帐号通过 SSH 登陆服务器, 没有登录的可以通过

1
$ ssh root@ip

出于安全考虑,不要使用 root 帐号运行 web 应用。这里新建一个专门用于部署的用户,例如 deploy 或者其它你喜欢的名字。运行以下命令创建用户:

1
# useradd -m -s /bin/bash deploy

将用户加入 sudo 群组,以便使用 sudo 命令:

1
# adduser deploy sudo

为 deploy 用户设置密码:

1
# passwd deploy

为deploy用户添加管理员权限

1
$ sudo echo "$USER ALL=(ALL:ALL) ALL" >> /etc/sudoers

退出当前 SSH 链接,用 deploy 帐号重新登陆。

1
ssh deploy@ip

安装 RVM 和 Ruby

更新 yum,并安装 curl:

1
2
$ sudo yum update
$ sudo yum install curl

然后安装 RVM:

1
2
3
$ gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
$ \curl -sSL https://get.rvm.io | bash -s stable
$ rvm requirements

RVM 安装完毕后,重新登陆 SSH,让 RVM 配置生效。然后安装 Ruby 2.1.2:

1
2
3
$ rvm use --install --default 2.2.2
$ rvm use 2.2.2@deploy --create --default
$ gem install bundle

安装完毕后,确认目前的 Ruby 版本:

1
$ ruby -v

应该看到 ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux] 字样。

安装 Passenger

Passenger 是一个 app server,支持基于 Rack 框架的 Ruby app(包括 Rails)。Passenger 的特点是需要作为模块编译到 Nginx 中,优点是配置简单,不需要自己写启动脚本。

安装 Passenger 最简单的方法是通过 yum 安装,首先导入 Passenger 的密钥(官方文档):

1
2
3
4
5
6
7
8
# 安装 EPEL and 和其他依赖
$ sudo yum install -y epel-release pygpgme curl

# 添加el7仓库
$ sudo curl --fail -sSLo /etc/yum.repos.d/passenger.repo https://oss-binaries.phusionpassenger.com/yum/definitions/el-passenger.repo

# 安装Passenger 和 Nginx, 由于万恶的墙,有可能失败,多试几次就可以
$ sudo yum install -y nginx passenger

现在修改 nginx 配置,编辑 /etc/nginx/conf.d/passenger.conf,找到这三行注释:

1
2
3
#passenger_root /usr/share/ruby/vendor_ruby/phusion_passenger/locations.ini;
#passenger_ruby /usr/bin/ruby;
#passenger_instance_registry_dir /var/run/passenger-instreg;

将它修改为:

1
2
3
passenger_root /usr/share/ruby/vendor_ruby/phusion_passenger/locations.ini;
passenger_ruby /usr/bin/ruby;
passenger_instance_registry_dir /var/run/passenger-instreg;

重启Nginx:

1
sudo service nginx restart

检查是否安装成功

1
sudo passenger-config validate-install

查看nginx和passgenr是否运行成功

1
sudo passenger-memory-stats

可以看到这个,说明已经成功运行

安装Nodejs

https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-a-centos-7-server

部署Rails项目

新建Rails项目

1
2
3
$ rails new app
$ cd app
$ bundle install

打开Gemfile文件,在Gemfiledevelopmenttestgroup 添加capistranorvm-capistrano

1
2
3
4
5
group :development, :test do
  # Use Capistrano for deployment
  gem 'capistrano', "~> 2.15.5"
  gem 'rvm-capistrano', '~> 1.4.1', :require => false
end

生成cap文件,在终端运行,会自动生成Capfileconfig/deploy.rb

1
$ capify .

修改deploy文件,配置项就不逐一介绍了,可以阅读 Capistrano 的文档 2.x Getting Started。主要是修改 :rvm_ruby_string 和 :repository 两个,其它再按需修改。

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
require 'rvm/capistrano'

set :rvm_type, :user

# repo details
set :scm, :git
# need to clean shared/cached-copy if changed repository
set :repository, "your repository url"
# set :branch, "master"
set :git_enable_submodules, 1

# bundler bootstrap
require 'bundler/capistrano'
set :bundle_without, [:darwin, :development, :test]

# Multi stage
# https://github.com/capistrano/capistrano/wiki/2.x-Multistage-Extension
# https://github.com/VinceMD/Scem/wiki/Deploying-on-production
set :stages, %w(production)
set :default_stage, "production" # require config/deploy/staging.rb
# set :default_stage, "production" # require config/deploy/production.rb
require 'capistrano/ext/multistage'

# server details
default_run_options[:pty] = true # apparently helps with passphrase prompting
ssh_options[:forward_agent] = true # tells cap to use my local private key
set :deploy_via, :remote_cache
set :use_sudo, false

# integrate whenever
# when using bundler
# set :whenever_command, "bundle exec whenever"
# when using different environments
# set :whenever_environment, defer { stage }
# set :whenever_identifier, defer { "#{fetch(:application)}-#{fetch(:rails_env)}" }
# require "whenever/capistrano"
# https://github.com/javan/whenever/blob/master/lib/whenever/capistrano.rb

# tasks
namespace :deploy do
  task :start, :roles => :app do
    run "touch #{current_path}/tmp/restart.txt"
  end

  task :stop, :roles => :app do
    # Do nothing.
  end

  desc "Restart Application"
  task :restart, :roles => :app do
    run "touch #{current_path}/tmp/restart.txt"
  end

  desc "Symlink shared resources on each release"
  task :symlink_shared, :roles => :app do
    %w{database settings.local}.each do |file|
      run "ln -nfs #{shared_path}/config/#{file}.yml #{release_path}/config/#{file}.yml"
    end

    # link dirs in public/
    %w{uploads}.each do |dir|
      run "mkdir -p #{shared_path}/public/#{dir}"
      run "ln -nfs #{shared_path}/public/#{dir} #{release_path}/public/#{dir}"
    end
  end

  desc "Initialize configuration using example files provided in the distribution"
  task :upload_config do
    %w{config}.each do |dir|
      run "mkdir -p #{shared_path}/#{dir}"
    end

    Dir["config/*.yml.example"].each do |file|
      top.upload(File.expand_path(file), "#{shared_path}/config/#{File.basename(file, '.example')}")
    end
  end

  desc 'Visit the app'
  task :visit_web do
    system "open #{app_url}"
  end
end

after 'deploy:setup', 'deploy:upload_config'
after 'deploy:update_code', 'deploy:symlink_shared'
after 'deploy:restart', 'deploy:visit_web'
after 'deploy:migrations', 'deploy:cleanup'

set :keep_releases, 7 # number for keeping old releases
after 'deploy', 'deploy:cleanup'

namespace :db do
  desc "Create db for current env"
  task :create do
    run "cd #{current_path}; bundle exec rake db:create RAILS_ENV=#{rails_env}"
    puts 'could be able to run `cap deploy:migrate` now'
  end

  desc "Populates the Production Database"
  task :seed do
    puts "\n\n=== Populating the Production Database! ===\n\n"
    run "cd #{current_path}; bundle exec rake db:seed RAILS_ENV=#{rails_env}"
  end
end

# http://guides.rubyonrails.org/asset_pipeline.html#precompiling-assets
# https://github.com/capistrano/capistrano/blob/master/lib/capistrano/recipes/deploy/assets.rb
load 'deploy/assets' unless (ARGV.join == "deploy:update" || ARGV.last == 'deploy:update')
# then we got these tasks:
# cap deploy:assets:clean      # Run the asset clean rake task.
# cap deploy:assets:precompile # Run the asset precompilation rake task.
namespace :remote do
  desc "Open the rails console on one of the remote servers"
  task :console, :roles => :app do
    hostname = find_servers_for_task(current_task).first
    command = "cd #{current_path} && bundle exec rails console #{fetch(:rails_env)}"
    if fetch(:rvm_ruby_string)
      # set rvm shell and get ride of "'"
      # https://github.com/wayneeseguin/rvm/blob/master/lib/rvm/capistrano.rb
      rvm_shell = %{rvm_path=$HOME/.rvm $HOME/.rvm/bin/rvm-shell "#{fetch(:rvm_ruby_string)}"}
      command = %{#{rvm_shell} -c "#{command}"}
    else
      command = %{source ~/.profile && "#{command}"}
    end
    exec %{ssh -l #{user} #{hostname} -t '#{command}'}
  end

  desc "run rake task. e.g.: `cap remote:rake db:version`"
  task :rake do
    ARGV.values_at(Range.new(ARGV.index('remote:rake')+1, -1)).each do |rake_task|
      top.run "cd #{current_path} && RAILS_ENV=#{rails_env} bundle exec rake #{rake_task}"
    end
    exit(0)
  end

  desc "run remote command. e.g.: `cap remote:run 'tail -n 10 log/production.log'`"
  task :run do
    command = ARGV.values_at(Range.new(ARGV.index('remote:run')+1, -1))
    top.run "cd #{current_path}; RAILS_ENV=#{rails_env} #{command*' '}"
    exit(0)
  end

  desc 'run specified rails code on server. e.g.: `cap remote:runner p User.all` or `cap remote:runner "User.all.each{ |u| p u }"`'
  task :runner do
    command=ARGV.values_at(Range.new(ARGV.index('remote:runner')+1,-1))
    top.run "cd #{current_path}; RAILS_ENV=#{rails_env} bundle exec rails runner '#{command*' '}'"
    exit(0)
  end

  desc "tail log on remote server"
  task :tail_log do
    top.run "tail -f #{current_path}/log/#{rails_env}.log" do |channel, stream, data|
      puts "#{data}"
      break if stream == :err
    end
    exit(0)
  end
end

namespace :update do
  desc "Dump remote database into tmp/, download file to local machine, import into local database"
  task :database do
    # config
    remote_db_yml_path          = "#{shared_path}/config/database.yml"
    remote_db_yml_on_local_path = "tmp/database_#{rails_env}.yml"

    # First lets get the remote database config file so that we can read in the database settings
    get remote_db_yml_path, remote_db_yml_on_local_path

    # load the remote settings within the database file
    remote_settings = YAML::load_file(remote_db_yml_on_local_path)[rails_env]

    remote_sql_file_path        = "#{current_path}/tmp/#{rails_env}-#{remote_settings["database"]}-dump.sql"
    remote_sql_gz_file_path     = "#{remote_sql_file_path}.gz"
    local_sql_file_path         = "tmp/#{rails_env}-#{remote_settings["database"]}-#{Time.now.strftime("%Y-%m-%d_%H:%M:%S")}.sql"
    local_sql_gz_file_path      = "#{local_sql_file_path}.gz"

    # we also need the local settings so that we can import the fresh database properly
    local_settings = YAML::load_file("config/database.yml")[rails_env]

    # dump the remote database and store it in the current path's tmp directory.
    run "mysqldump -u'#{remote_settings["username"]}' -p'#{remote_settings["password"]}' #{"-h '#{remote_settings["host"]}'" if remote_settings["host"]} '#{remote_settings["database"]}' > #{remote_sql_file_path}"

    # gzip db
    run "gzip -f #{remote_sql_file_path}"

    # download gz file to local
    get remote_sql_gz_file_path, local_sql_gz_file_path

    # unzip sql
    run_locally "gunzip #{local_sql_gz_file_path}"

    # import db to local db
    # may need to run `RAILS_ENV=production rake db:create` on local first
    run_locally("mysql -u#{local_settings["username"]} #{"-p#{local_settings["password"]}" if local_settings["password"]} #{local_settings["database"]} < #{local_sql_file_path}")

    # now that we have the upated production dump file we should use the local settings to import this db.
  end

  desc "Mirrors the remote shared public directory with your local copy, doesn't download symlinks"
  task :shared_assets do
    run_locally "if [ -e public/uploads ]; then mv public/uploads public/uploads_back; fi"
    # using rsync so that it only copies what it needs
    run_locally("rsync --recursive --times --rsh=ssh --compress --human-readable --progress #{user}@#{app_server}:#{shared_path}/system/ public/system/")

    run_locally("rsync --recursive --times --rsh=ssh --compress --human-readable --progress #{user}@#{app_server}:#{shared_path}/public/uploads/ public/uploads/")
  end

  namespace :remote do
    desc "update the remote database with the local database"
    task :database do
      input = ''
      # STDOUT.puts "Are you SURE to update the databse of remote?(YES)"
      # confirmation = STDIN.gets.chomp
      confirmation = Capistrano::CLI.ui.ask("Are you SURE to update the databse of remote?(YES)")
      abort "Interrupt.." unless confirmation == "YES"
      # config database.yml on both sides
      remote_db_yml_path          = "#{shared_path}/config/database.yml"
      remote_db_yml_on_local_path = "tmp/database_#{rails_env}.yml"

      # First get the local database config to remote
      get remote_db_yml_path, remote_db_yml_on_local_path

      # load the local settings within the database file
      local_settings = YAML::load_file("config/database.yml")[rails_env]

      # set the sql path on both sides
      local_sql_file_path = "tmp/#{rails_env}-#{local_settings['database']}-dump.sql"
      local_sql_gz_file_path = "#{local_sql_file_path}.gz"
      remote_sql_file_path = "#{current_path}/tmp/#{rails_env}-#{local_settings['database']}-#{Time.now.strftime("%Y-%m-%d_%H:%M:%S")}.sql"
      remote_sql_gz_file_path = "#{remote_sql_file_path}.gz"

      # we also need the remote settings so that we can import the fresh dataabse properly
      remote_settings = YAML::load_file(remote_db_yml_on_local_path)[rails_env]

      # dump the local database and store it in the tmp dir
      if local_settings['adapter'] == 'postgresql'
        run_locally "PGPASSWORD='#{local_settings['password']}' pg_dump  -U #{local_settings["username"]} #{"-h '#{local_settings["host"]}'" if local_settings["host"]} -c -O '#{local_settings["database"]}' > #{local_sql_file_path}"
      elsif local_settings['adapter'] == 'mysql2'
        run_locally "mysqldump -u'#{local_settings["username"]}' #{"-p#{local_settings["password"]}" if local_settings["password"]} #{"-h '#{local_settings["host"]}'" if local_settings["host"]} '#{local_settings["database"]}' > #{local_sql_file_path}"
      else
        raise "not supports #{local_settings['adapter']}"
      end

      # gzip db
      run_locally "gzip -f #{local_sql_file_path}"

      # send the gz file to remote
      upload local_sql_gz_file_path, remote_sql_gz_file_path

      # unzip sql
      run "gunzip #{remote_sql_gz_file_path}"

      # import db to remote db
      # may need to run `RAILS_ENV=production rake db:create` on remote first
      if local_settings['adapter'] == 'postgresql'
        run "PGPASSWORD='#{remote_settings['password']}' psql -U #{remote_settings['username']} -d #{remote_settings["database"]} -f #{remote_sql_file_path}"
      elsif local_settings['adapter'] == 'mysql2'
        run "mysql -u#{remote_settings["username"]} #{"-p#{remote_settings["password"]}" if remote_settings["password"]} #{remote_settings["database"]} < #{remote_sql_file_path}"
      else
        raise "not supports #{local_settings['adapter']}"
      end

      # now that we have the updated production dump file we should use the remote settings to import this db
    end

    desc "Mirrors the local shared public directory with the remote copy, doesn't download symlinks"
    task :shared_assets do
      run "cp -R #{shared_path}/system #{shared_path}/system_back"
      run "cp -R #{shared_path}/public/uploads/ #{shared_path}/public/uploads_back"
      run_locally("rsync --recursive --times --rsh=ssh --compress --human-readable --progress public/system #{user}@#{app_server}:#{shared_path}/")
      run_locally("rsync --recursive --times --rsh=ssh --compress --human-readable --progress public/uploads/ #{user}@#{app_server}:#{shared_path}/public/uploads")
    end
  end
end

添加deploy/production.rb,并填写以下内容

1
2
3
4
5
6
7
8
9
10
11
12
# config/deploy/production.rb
set :app_server, "服务器地址"
set :app_url, "服务器网址"
set :application, app_server
role :web, app_server
role :app, app_server
role :db,  app_server, :primary => true
set :deploy_to, "/var/www/xxx"
set :user, "deploy"
set :rvm_ruby_string, "ruby-2.2.2"
set :branch, "master"
set :rails_env, "productiono"

添加database.yml.example

config/database.yml文件是不应该被提交到代码库的,我们需要用 config/database.yml.example 来做样本,在部署的时候它会被上传到服务器的 /path_to_your_site/shared/config/ 目录里,然后 SSH 到服务器上修改 username, password, socket 等值(上传的动作是在测试部署里说明)。尤其注意 MySQL socket 的路径在 Mac OS 和 Ubuntu 系统上是不同的。

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
# SQLite version 3.x
#   gem install sqlite3
#
#   Ensure the SQLite 3 gem is defined in your Gemfile
#   gem 'sqlite3'
#
default: &default
  adapter: sqlite3
  pool: 5
  timeout: 5000

development:
  <<: *default
  database: db/development.sqlite3

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: db/test.sqlite3

production:
  <<: *default
  database: db/production.sqlite3

production添加secret_key_base

编辑 config/secrets.yml 文件,添加:

1
2
production:
  secret_key_base: a1feaf0cb8501bb41952bbd1df3641ca0c4eed5beb19fcb69bbaaf30b5e11d58872092a84951679f7df622dda2d6bd1b21f98bd2a24b99e096095f003e4ccf90

128位的随机字符串可用 rake secret 命令生成。

在你的github或者gitlab添加ssh key

你可以按如下命令来生成sshkey

1
2
ssh-keygen -t rsa -C "xxxxx@xxxxx.com"
cat ~/.ssh/id_rsa.pub

初始化cap

1
2
3
4
5
cap deploy:setup
cap deploy:check
cap deploy:update
cap db:create
cap deploy:migrate

以后就可以通过跑以下命令,部署你的应用了

1
cap deploy:migrations 或者 cap deploy

修改 Nginx 配置

删除原有的默认网站配置:

1
$ rm /etc/nginx/sites-enabled/default

新建网站配置:

1
$ touch /etc/nginx/sites-enabled/example.com.conf

编辑/etc/nginx/sites-enabled/example.com.conf,写入以下内容:

1
2
3
4
5
6
server {
    listen 80 default;
    server_name example.com; # 这里填写你真实域名
    root /var/www/example.com/current/public;
    passenger_enabled on;
}

重启Nginx

1
$ sudo service nginx restart

完成

在浏览器打开服务器的 IP 地址或域名,可以看到Nginx已经启动

ref: http://stackoverflow.com/questions/17413526/nginx-missing-sites-available-directory

Comments