Deploy Rails with PostgreSQL Using Passenger and Nginx on CentOS
最近读完了 Michael Hartl 的《Ruby on Rails Tutorial》,于是想试着在自己的 Digital Ocean (CentOS 7) 上部署一下最终完成的 sample app。最简单的方法当然是:
$ rails server -e production -b 0.0.0.0 -p 80
但 Rails 内建的 WEBrick + SQLite 性能较差,而且这样部署也不够灵活,还是上 Apache / Nginx 比较主流。与 Apache 相比 Nginx 的异步模型能更好地处理高并发的场景,Passenger 性能不错并且可以直接集成于 Apache / Nginx 而不必反向代理,而 PostgreSQL 比 MySQL / MariaDB 功能更强大,这三者是 Ruby 服务器的最佳实践之一,因此我决定折腾一下 Nginx + Passenger + PostgreSQL。
安装 Ruby
直接使用 yum 安装当然是可行的,但 Ruby Version Manager (RVM) 能够更方便地安装并管理 Ruby 版本,因此下面以 RVM 为例。安装 RVM:
$ gpg --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
$ curl -sSL https://get.rvm.io | bash -s stable
接着就能轻松地安装任意版本的 Ruby 了(rvm list known
可以列出已知的版本):
$ rvm install X.X.X
$ rvm --default use X.X.X
为了管理项目依赖,还需要安装一下 Bundler:
$ gem install bundler --no-rdoc --no-ri
如果服务器在国内,由于众所周知的原因,也许会频繁遭遇网络连接失败,这时候可以考虑使用 Ruby China 的 Ruby 源代码镜像 和 RubyGems 镜像:
$ echo "ruby_url=https://cache.ruby-china.org/pub/ruby" > $rvm_path/user/db
$ gem sources -a https://gems.ruby-china.org/ -r https://rubygems.org/
$ bundle config mirror.https://rubygems.org https://gems.ruby-china.org
安装 JavaScript 引擎
如果 Gemfile 直接或间接依赖了 ExecJS,譬如 coffee-rails
/ turbolinks
/ uglifier
/ bootstrap-sass
等等,则还需要安装 JS 引擎,以在 Ruby 中执行 JS 代码。
- 一种方法是在 Gemfile 中加上
gem 'therubyracer'
,这是一个嵌入式的 V8 引擎。 - 另一种方法是直接在服务器上安装 Node.js,这使用的也是 V8 引擎。安装 Extra Packages for Enterprise Linux (EPEL) 后可以直接通过 yum 安装 node,但版本会比较旧;与 RVM 类似,Node.js 也有 NVM 来安装和管理版本,这里不再赘述。
安装 Nginx + Passenger
yum 原本的源中没有 Nginx 和 Passenger,可以导入 Passenger 自己的源,安装已包含 Passenger 模块的 Nginx 版本(这样就不必去运行 passenger-install-nginx-module
了)和 Passenger 本体:
$ curl --fail -sSLo /etc/yum.repos.d/passenger.repo https://oss-binaries.phusionpassenger.com/yum/definitions/el-passenger.repo
$ yum -y install nginx passenger
接着在 /etc/nginx/conf.d/passenger.conf
中解除下面三行的注释:
passenger_root /usr/share/ruby/vendor_ruby/phusion_passenger/locations.ini;
passenger_ruby /usr/bin/ruby;
passenger_instance_registry_dir /var/run/passenger-instreg;
安装 PostgreSQL
先从 yum 安装 PostgreSQL:
$ yum -y install postgresql-server postgresql-devel
$ postgresql-setup initdb
接着用 systemd(在 CentOS 7 中它取代了 Upstart 和 SysVinit)启动数据库并设为开机启动项:
$ systemctl start postgresql
$ systemctl enable postgresql
PostgreSQL 默认的超级用户是 postgres
,使用同名的操作系统账户可以直接登录,这里为数据库新建一个用户,该用户应与后文的 Linux 用户同名以便免密码连接数据库:
$ su - postgres
$ createuser sample-app
$ createdb -O sample-app sample-app
Gemfile 中也需要加入 PostgreSQL 的接口:
gem 'pg', group: :production
如果创建 app 的时候没有指定数据库(譬如 rails new sample-app -d postgresql
),那么 Rails 默认使用的是 SQLite 3,我们还需要提前修改一下 Rails 的配置文件 config/database.yml
:
production:
adapter: postgresql
encoding: unicode
pool: 5
database: sample-app
username: sample-app
部署 Rails
为了安全考虑,最好为每个 app 创建一个同名的 Linux 用户,Passenger 也提供了相应的用户账户沙盒功能,默认会以 config.ru
的所有者运行 app。下面的部署工作都在该账户下完成(以下假设 app 在 /srv/sample-app
):
$ adduser sample-app
$ chown -R sample-app: /srv/sample-app
$ su - sample-app
首先是通过 Bundler 安装 app 的依赖项,因为是部署模式所以 Gemfile.lock
应当提前已经生成好:
$ cd /srv/sample-app
$ bundle install --deployment --without development test
接下来检查一下 config/database.yml
和 config/secrets.yml
,前者即上文提到的数据库配置,后者存储的是用来加密 cookies 的密钥,生产环境默认从环境变量读取密钥,以防开发者手滑将其上传到 public repo:
# Do not keep production secrets in the repository, instead read values from the environment.
production:
secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
密钥可通过 bundle exec rake secret
生成,至于如何写入环境变量,包括但不限于以下几种方式:
- 把
export SECRET_KEY_BASE=...
直接写进~/.bashrc
或是其他 shell 的启动文件; - 在 Gemfile 中加入 dotenv-rails,使用
.env.production
文件管理生产环境; - 不少开发者会使用 Foreman 启动各种服务,它会自动读取
.env
文件; - 在集成了 Passenger 的 Nginx 配置文件中加上
passenger_env_var SECRET_KEY_BASE ...
。
当然单单在 shell 里敲一行 export 命令是不行的,这只会在当前 shell 及其子进程中生效。另外不要随意更改密钥,这会使原先加密的 cookies 失效。
最后让 Asset Pipeline 对静态资源做预编译,让 Active Record 做数据库迁移,别忘了指定生产环境:
$ bundle exec rake assets:precompile db:migrate RAILS_ENV=production
启动 Nginx
回到管理员账户,创建 Nginx 配置文件 /etc/nginx/conf.d/sample-app.conf
,为自己的 app 添加一个虚拟主机,其中 passenger_ruby
的路径可以通过 passenger-config about ruby-command
命令获知,passenger_app_root
默认是虚拟主机 root
的上级目录:
server {
listen 80;
server_name sample-app.example.com;
root /srv/sample-app/public;
passenger_enabled on;
passenger_ruby /usr/local/rvm/gems/ruby-X.X.X/wrappers/ruby;
}
/etc/nginx/nginx.conf
里默认有一行 include /etc/nginx/conf.d/*.conf;
所以我们不需要去更改其他配置。于是现在可以正式启动 Nginx 了:
$ systemctl start nginx
$ systemctl enable nginx
理论上 app 已经部署成功了~ 🎉
之后如果需要让 Passenger 重启 app,或是更改了配置需要重载 Nginx:
$ touch /srv/sample-app/tmp/restart.txt
$ systemctl reload nginx
常见问题
- 如果在本地测试生产环境时,发现静态文件未能加载:
- 其一可能是因为没有做预编译,Rails 认为生产环境的静态资源应该已经事先编译好了,所以 Asset Pipeline 的实时编译是关闭的;
- 其二可能是因为 Rails 默认没有启用静态文件服务,可以通过设置
RAILS_SERVE_STATIC_FILES
环境变量来开启它,但这在生产环境中低效且不安全,应该让 Nginx / Apache 来处理它们。 - 上述默认配置均在
config/environments/production.rb
文件中:
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
# Disable serving static files from the `/public` folder by default since Apache or NGINX already handles this.
config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
- 访问网站时显示「Incomplete response received from application」一般是没有设置
secret_key_base
,具体参见前文「部署 Rails」。 - 如果在
gem install pg
时报告找不到头文件libpq-fe.h
,那是因为没有yum install postgresql-devel
,或者在 Debian 系的 APT 中这个包叫libpq-dev
。 - 如果在
rake assets:precompile
时显示「Killed」,很可能是内存不足导致的。最简单的解决方法就是花钱💰去升级 VPS 的内存,或者可以创建 SWAP 文件用于虚拟内存:
$ fallocate -l 1G /swapfile
$ chmod 600 /swapfile
$ mkswap /swapfile
$ swapon /swapfile
$ echo "/swapfile swap swap defaults 0 0" >> /etc/fstab
- Bundler 会将所有
--without
的组保存在.bundle/config
中,因此之后的bundle install
永远不会包含这些组。如果被这个特性所困扰,可以通过删除配置文件相关内容或使用--with
选项来解决。
测试环境
CentOS 7 x86_64 (1511) / Nginx 1.8.1 / Passenger 5.0.24 / PostgreSQL 9.2.14 / Bundler 1.11.2 / Rails 4.2.5 / Ruby 2.3.0
参考文档:
Deploying a Ruby app with Passenger to production - Passenger Library
Configuration reference for Nginx - Passenger Library
Configuring NGINX Plus as a Web Server | NGINX Admin Guide
PostgreSQL Documentation: Tutorial
Configuring Rails Applications - Ruby on Rails Guides
Ruby on Rails Security Guide - Ruby on Rails Guides
Chapter 14. Swap Space - Storage Administration Guide - Red Hat Enterprise Linux 7