如何在一台全新新的主机 Rails 服务呢? 有什么比较好的策略? 有哪些问题需要我们注意?
这篇文章转载于同事(小光)的经验总结,特在此分享。如果在你实践过程中,发现有部分内容过时,请告知一下,谢谢。
0. Setup user for deployment
# Install openssh-server
sudo apt-get update
sudo apt-get install openssh-server # This step may not required in Aliyun host
# Setup deployment user
sudo adduser deploy # Add a user for deployment
sudo usermod -a -G sudo deploy
#copy pub key to server
ssh deploy@host 'mkdir -p .ssh && cat >> .ssh/authorized_keys' < ~/.ssh/id_rsa.pub
cd /etc/ssh
sudo vi sshd_config
# modify sshd_config
PermitRootLogin no
RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile %h/.ssh/authorized_keys
# restart ssh server
sudo service ssh restart
1. Install packages
sudo apt-get update
sudo apt-get install -y build-essential openssl curl libcurl3-dev libreadline6 libreadline6-dev git zlib1g zlib1g-dev libssl-dev libyaml-dev libxml2-dev libxslt-dev autoconf automake libtool imagemagick libmagickwand-dev libpcre3-dev libsqlite3-dev libmysql-ruby libmysqlclient-dev
sudo apt-get install git
# 添加ssh_key到github账号上
git config --global user.name "lohaswork"
git config --global user.email "support@lohaswork.com"
ssh-keygen -t rsa -C "support@lohaswork.com"
cat ~/.ssh/id_rsa.pub //复制pub-key至github账号设定的ssh keys中
ssh -T git@github.com //测试ssh至github是否成功
# Make deployment directory
sudo mkdir /www
sudo mkdir /www/teamind_deploy
sudo chown -R deploy /www/teamind_deploy
cd home/deploy
git clone git://github.com/sstephenson/rbenv.git ~/.rbenv
git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
git clone git://github.com/sstephenson/rbenv-gem-rehash.git ~/.rbenv/plugins/rbenv-gem-rehash
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> .profile
echo 'export PATH="$HOME/.rbenv/shims:$PATH"' >> .profile
echo 'eval "$(rbenv init -)"' >> .profile
source ~/.bash_profile
# Reboot
sudo reboot
# Upload ruby_build binary package to server using sftp and then
mkdir ~/.rbenv/cache
cp ruby-1.9.3-p448.tar.gz ~/.rbenv/cache
cp yaml-0.1.4.tar.gz ~/.rbenv/cache
rbenv install 1.9.3-p448
rbenv global 1.9.3-p448
gem source -r https://rubygems.org/
gem source -a http://ruby.taobao.org/
# 在 ~/.gemrc 中配置不生成 ri 和 rdoc 文档
$ vi ~/.gemrc
# add following 2 lines to .gemrc
install: --no-rdoc --no-ri
update: --no-rdoc --no-ri
gem install bundler
sudo apt-get install python-software-properties
sudo apt-add-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs
sudo add-apt-repository ppa:nginx/stable
sudo apt-get update
sudo apt-get install nginx
sudo service nginx start
cd /etc/nginx/sites-enabled/
sudo rm default
2. Problems with locale
# ls 初始化月份字符串出错
sudo vi /var/lib/locales/supported.d/local
# 改成如下内容
en_US.UTF-8 UTF-8
zh_CN.UTF-8 UTF-8
zh_CN.GBK GBK
zh_CN GB2312
# 执行
sudo locale-gen
sudo vi /etc/default/locale
# 改成如下内容
LANG="zh_CN.UTF-8"
LANGUAGE="zh_CN.UTF-8"
LC_ALL="zh_CN.UTF-8"
3. Install database
# PostgreSQL
sudo apt-get install postgresql-9.1
sudo apt-get install postgresql-client-9.1 postgresql-contrib-9.1 postgresql-server-dev-9.1
# 配置postgres部署时的用户
sudo passwd postgres
sudo su postgres
postgres=# psql
postgres=# ALTER USER postgres PASSWORD 'postgres'; // 修改密码
postgres=# CREATE USER deploy WITH PASSWORD 'lohaswork' CREATEDB; // 创建deploy用户
postgres=# \q // 退出
exit //go back from user postgres to deploy
sudo service postgresql start
# MySQL
sudo apt-get install mysql-server-5.5 mysql-client-5.5
mysql -uroot -p
mysql> CREATE USER 'deploy'@'localhost' IDENTIFIED BY 'lohaswork';
mysql> GRANT ALL PRIVILEGES ON *.* TO 'deploy'@'localhost' WITH GRANT OPTION;
mysql> \q
4. Autoload services
# unicorn
cd /etc/init.d
sudo touch nicorn.teamind_deploy
sudo vi unicorn.teamind_deploy
# add following lines to unicorn.teamind_deploy
#!/bin/sh
set -e
# Example init script, this can be used with nginx, too,
# since nginx and unicorn accept the same signals
# Feel free to change any of the following variables for your app:
TIMEOUT=${TIMEOUT-60}
APP_ROOT=/www/teamin_deploy/current
PID=$APP_ROOT/tmp/pids/unicorn.pid
CMD="/usr/bin/unicorn -D -c $APP_ROOT/config/unicorn.rb"
action="$1"
set -u
old_pid="$PID.oldbin"
cd $APP_ROOT || exit 1
sig () {
test -s "$PID" && kill -$1 `cat $PID`
}
oldsig () {
test -s $old_pid && kill -$1 `cat $old_pid`
}
case $action in
start)
sig 0 && echo >&2 "Already running" && exit 0
$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"
$CMD
;;
upgrade)
if sig USR2 && sleep 2 && sig 0 && oldsig QUIT
then
n=$TIMEOUT
while test -s $old_pid && test $n -ge 0
do
printf '.' && sleep 1 && n=$(( $n - 1 ))
done
echo
if test $n -lt 0 && test -s $old_pid
then
echo >&2 "$old_pid still exists after $TIMEOUT seconds"
exit 1
fi
exit 0
fi
echo >&2 "Couldn't upgrade, starting '$CMD' instead"
$CMD
;;
reopen-logs)
sig USR1
;;
*)
echo >&2 "Usage: $0 <start|stop|restart|upgrade|force-stop|reopen-logs>"
exit 1
;;
esac
5. deploy.rb nginx.conf unicorn.rb capistrano-db-rollback.rb capistrano_database_yml.rb config.ru
#### deploy.rb
require 'bundler/capistrano'
$:.unshift('./config')
require 'capistrano-db-rollback'
require 'capistrano_database_yml'
# Five steps to run the first deployment
# 0. Create unicorn related files at local development ENV
# 1. Create deploy_user in linux and install packages
# 2. Manually create deploy_user in postgres and create www/#{appname} directory
# 3. Modify server info in deploy.rb & nginx.conf & capistrano_database_yml.rb
# 4. Run deploy:setup and config database following the leading message
# 5. !Important: Run cap deploy:cold for the very first deployment
# 6. Upload the video manually
# Need change before deployment
set :server_name, "192.168.1.114"
set :user, "deploy"
set :sudo_user, "deploy"
set :deploy_to, "/www/teamind_deploy"
# Repository
set :application, "LohasWork.com"
set :scm, :git
set :repository, "git@github.com:lohaswork/LohasWork.com"
set :branch, "serco/for-deploy" # Need changge to master
# Configurations
set :rails_env, "production"
set :deploy_via, :remote_cache
set :use_sudo, false
set :normalize_asset_timestamps, false
default_run_options[:pty] = true
set :rbenv_version, ENV['RBENV_VERSION'] || "1.9.3-p327"
set :default_environment, {
'PATH' => "/home/#{user}/.rbenv/shims:/home/#{user}/.rbenv/bin:$PATH",
'RBENV_VERSION' => "#{rbenv_version}",
}
# Roles
role :web, "192.168.1.114" # Your HTTP server, Apache/etc
role :app, "192.168.1.114" # This may be the same as your `Web` server
role :db, "192.168.1.114", :primary => true # This is where Rails migrations will run
# For Unicorn service
set :unicorn_config, "#{current_path}/config/unicorn.rb"
set :unicorn_pid, "#{current_path}/tmp/pids/unicorn.pid"
namespace :deploy do
task :cold do # Overriding the default deploy:cold
update
setup_db # My own step, replacing migrations.
start
end
task :add_shared do
run "mkdir -p #{shared_path}/public"
run "chmod g+rx,u+rwx #{shared_path}/public"
run "mkdir -p #{shared_path}/public/videos"
run "chmod g+rx,u+rwx #{shared_path}/public/videos"
run "mkdir -p #{shared_path}/"
run "chmod g+rx,u+rwx #{shared_path}/tmp"
run "mkdir -p #{shared_path}/unicorn"
run "chmod g+rx,u+rwx #{shared_path}/unicorn"
run "cd #{shared_path}"
run "touch err.log out.log"
run "mkdir -p #{shared_path}/sockets"
run "chmod g+rx,u+rwx #{shared_path}/sockets"
run "mkdir -p #{shared_path}/tmp/sessions"
run "chmod g+rx,u+rwx #{shared_path}/tmp/sessions"
run "mkdir -p #{shared_path}/tmp/cache"
run "chmod g+rx,u+rwx #{shared_path}/tmp/cache"
end
task :start, :roles => :app, :except => { :no_release => true } do
run "cd #{current_path} && RAILS_ENV=production bundle exec unicorn_rails -c #{unicorn_config} -D"
end
task :stop, :roles => :app, :except => { :no_release => true } do
run "if [ -f #{unicorn_pid} ]; then kill -QUIT `cat #{unicorn_pid}`; fi"
end
task :restart, :roles => :app, :except => { :no_release => true } do
# 用USR2信号来实现无缝部署重启
run "if [ -f #{unicorn_pid} ]; then kill -s USR2 `cat #{unicorn_pid}`; fi"
end
desc 'clean old files, link shared files'
task :housekeeping, :roles => :app do
run "rm -rf #{current_path}/public/videos" ###
run "ln -s #{shared_path}/public/videos #{current_path}/public/videos"
run "#{sudo} ln -nfs #{current_path}/config/nginx.conf /etc/nginx/sites-enabled/nginx.conf"
run "rm -rf #{current_path}/unicorn"
run "ln -s #{shared_path}/unicorn/ #{current_path}/unicorn"
run "rm -rf #{current_path}/tmp/sockets"
run "ln -s #{shared_path}/sockets #{current_path}/tmp/sockets"
run "rm -rf #{current_path}/tmp/sessions"
run "ln -s #{shared_path}/tmp/sessions #{current_path}/tmp/sessions"
run "rm -rf #{current_path}/tmp/cache"
run "ln -s #{shared_path}/tmp/cache #{current_path}/tmp/cache"
end
# utilize that capistrano has already done this!
#
# If you put your shared file or folder here:
# /path/to/app/shared/sockets
# Then it will be symlinked here:
# /path/to/app/releases/20120517191233/tmp/sockets
#
shared_children.push "tmp/sockets"
shared_children.push "unicorn"
task :nginx_restart, :roles => :app do
run "#{sudo} service nginx restart"
end
task :setup_db, :roles => :app do
raise RuntimeError.new('db:setup aborted!') unless Capistrano::CLI.ui.ask("About to `rake db:setup`. Are you sure to wipe the entire database (anything other than 'yes' aborts):") == 'yes'
run "cd #{current_path}; bundle exec rake db:setup RAILS_ENV=#{rails_env}"
end
end
after 'deploy:setup', 'deploy:add_shared'
after 'deploy:create_symlink', 'deploy:housekeeping'
after 'deploy:restart', 'deploy:cleanup'
#### nginx.conf
#每台机器都运行nginx+unicorn,本机用domain socket,方便切换
upstream ruby_backend {
server unix:/tmp/unicorn.sock fail_timeout=0;
}
#用try_files方式和proxy执行rails动态请求
server {
listen 80;
server_name 192.168.0.105;
root /www/teamind_deploy/current/public;
try_files $uri/index.html $uri.html $uri @httpapp;
location @httpapp {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering on;
proxy_pass http://ruby_backend;
}
}
unicorn.rb
worker_processes 6 #依据需要的内存数量设定
app_root = File.expand_path("../..", __FILE__)
working_directory app_root
# Listen on fs socket for better performance
listen "/tmp/unicorn.sock", :backlog => 64
listen 4096, :tcp_nopush => false
# Nuke workers after 30 seconds instead of 60 seconds (the default)
timeout 30
# App PID
pid "#{app_root}/tmp/pids/unicorn.pid"
# By default, the Unicorn logger will write to stderr.
# Additionally, some applications/frameworks log to stderr or stdout,
# so prevent them from going to /dev/null when daemonized here:
stderr_path "#{app_root}/log/unicorn.stderr.log"
stdout_path "#{app_root}/log/unicorn.stdout.log"
# To save some memory and improve performance
preload_app true
GC.respond_to?(:copy_on_write_friendly=) and
GC.copy_on_write_friendly = true
# Force the bundler gemfile environment variable to
# reference the Сapistrano "current" symlink
before_exec do |_|
ENV["BUNDLE_GEMFILE"] = File.join(app_root, 'Gemfile')
end
before_fork do |server, worker|
# 参考 http://unicorn.bogomips.org/SIGNALS.html
# 使用USR2信号,以及在进程完成后用QUIT信号来实现无缝重启
old_pid = app_root + '/tmp/pids/unicorn.pid.oldbin'
if File.exists?(old_pid) && server.pid != old_pid
begin
Process.kill("QUIT", File.read(old_pid).to_i)
rescue Errno::ENOENT, Errno::ESRCH
# someone else did our job for us
end
end
# the following is highly recomended for Rails + "preload_app true"
# as there's no need for the master process to hold a connection
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
end
after_fork do |server, worker|
# 禁止GC,配合后续的OOB,来减少请求的执行时间
GC.disable
# the following is *required* for Rails + "preload_app true",
defined?(ActiveRecord::Base) and
ActiveRecord::Base.establish_connection
end
capistrano-db-rollback.rb
configuration = Capistrano::Configuration.respond_to?(:instance) ?
Capistrano::Configuration.instance(:must_exist) :
Capistrano.configuration(:must_exist)
configuration.load do
namespace :deploy do
namespace :rollback do
desc <<-DESC
Rolls back the migration to the version found in schema.rb file of the previous release path.\
Uses sed command to read the version from schema.rb file.
DESC
task :migrations do
run "cd #{current_release}; rake db:migrate RAILS_ENV=#{rails_env} VERSION=`grep \":version =>\" #{previous_release}/db/schema.rb | sed -e 's/[a-z A-Z = \> \: \. \( \)]//g'`"
end
after "deploy:rollback","deploy:rollback:migrations"
end
end
end
capistrano_database_yml.rb
#
# = Capistrano database.yml task
#
# Provides a couple of tasks for creating the database.yml
# configuration file dynamically when deploy:setup is run.
#
# Category:: Capistrano
# Package:: Database
# Author:: Simone Carletti <weppos@weppos.net>
# Copyright:: 2007-2010 The Authors
# License:: MIT License
# Link:: http://www.simonecarletti.com/
# Source:: http://gist.github.com/2769
#
#
# == Requirements
#
# This extension requires the original <tt>config/database.yml</tt> to be excluded
# from source code checkout. You can easily accomplish this by renaming
# the file (for example to database.example.yml) and appending <tt>database.yml</tt>
# value to your SCM ignore list.
#
# # Example for Subversion
#
# $ svn mv config/database.yml config/database.example.yml
# $ svn propset svn:ignore 'database.yml' config
#
# # Example for Git
#
# $ git mv config/database.yml config/database.example.yml
# $ echo 'config/database.yml' >> .gitignore
#
#
# == Usage
#
# Include this file in your <tt>deploy.rb</tt> configuration file.
# Assuming you saved this recipe as capistrano_database_yml.rb:
#
# require "capistrano_database_yml"
#
# Now, when <tt>deploy:setup</tt> is called, this script will automatically
# create the <tt>database.yml</tt> file in the shared folder.
# Each time you run a deploy, this script will also create a symlink
# from your application <tt>config/database.yml</tt> pointing to the shared configuration file.
#
# === Password prompt
#
# For security reasons, in the example below the password is not
# hard coded (or stored in a variable) but asked on setup.
# I don't like to store passwords in files under version control
# because they will live forever in your history.
# This is why I use the Capistrano::CLI utility.
#
unless Capistrano::Configuration.respond_to?(:instance)
abort "This extension requires Capistrano 2"
end
Capistrano::Configuration.instance.load do
namespace :deploy do
namespace :db do
desc <<-DESC
Creates the database.yml configuration file in shared path.
When this recipe is loaded, db:setup is automatically configured \
to be invoked after deploy:setup. You can skip this task setting \
the variable :skip_db_setup to true. This is especially useful \
if you are using this recipe in combination with \
capistrano-ext/multistaging to avoid multiple db:setup calls \
when running deploy:setup for all stages one by one.
DESC
task :setup, :except => { :no_release => true } do
# default_template = <<-EOF
# base: &base
# adapter: postgresql
# username: #{Capistrano::CLI.ui.ask("Enter database username: ")}
# password: #{Capistrano::CLI.password_prompt("Enter database password: ")}
# pool: 5
# development:
# database: lahaswork_development
# <<: *base
# test:
# database: lohaswork_test
# <<: *base
# production:
# database: lohaswork_production
# <<: *base
# EOF
default_template = <<-EOF
base: &base
adapter: mysql2
encoding: utf8
hostname: localhost
username: #{Capistrano::CLI.ui.ask("Enter database username: ")}
password: #{Capistrano::CLI.password_prompt("Enter database password: ")}
pool: 5
development:
database: lahaswork_development
<<: *base
test:
database: lohaswork_test
<<: *base
production:
database: lohaswork_production
<<: *base
EOF
location = fetch(:template_dir, "config/deploy") + '/database.yml.erb'
template = File.file?(location) ? File.read(location) : default_template
config = ERB.new(template)
run "mkdir -p #{shared_path}/db"
run "mkdir -p #{shared_path}/config"
run "chmod g+rx,u+rwx #{shared_path}/config"
put config.result(binding), "#{shared_path}/config/database.yml"
end
desc <<-DESC
[internal] Updates the symlink for database.yml file to the just deployed release.
DESC
task :symlink, :except => { :no_release => true } do
run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml"
end
end
after "deploy:setup", "deploy:db:setup" unless fetch(:skip_db_setup, false)
after "deploy:finalize_update", "deploy:db:symlink"
end
end
config.ru
# This file is used by Rack-based servers to start the application.
require 'unicorn/oob_gc'
require 'unicorn/worker_killer'
#每10次请求,才执行一次GC
use Unicorn::OobGC, 10
#设定最大请求次数后自杀,避免禁止GC带来的内存泄漏(3072~4096之间随机,避免同时多个进程同时自杀,可以和下面的设定任选)
use Unicorn::WorkerKiller::MaxRequests, 3072, 4096
#设定达到最大内存后自杀,避免禁止GC带来的内存泄漏(192~256MB之间随机,避免同时多个进程同时自杀)
use Unicorn::WorkerKiller::Oom, (192*(1024**2)), (256*(1024**2))
require ::File.expand_path('../config/environment', __FILE__)
run LohasWorkCom::Application