<-- home

在 ubuntu 20.04 部署 rails production

因为一台腾讯云服务器最近到期了,所以想借着重新部署的机会,把部署 Rails 的整个流程写成文档,同时把 Rails 和 Ruby 版本也进行升级,

这次部署,同时把老项目的 mina 部署改为了 capistrano 部署。

如果进入服务器发现没有 .bashrc,从 /etc/skel/.bashrc 复制出来一个

以下操作都是在默认用户下安装,也可以自己创建一个 deploy 用户


安装 ruby 环境

# rbevn
$ curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-installer | bash

$ rbenv init

$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc
$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
$ echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc

# ruby 和 gem 替换国内镜像源
# https://github.com/andorchen/rbenv-china-mirror
$ export RUBY_BUILD_MIRROR_URL=https://cache.ruby-china.com

$ rbenv install 2.7.2

# update gem
gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/
gem sources -l
https://gems.ruby-china.com
# make sure only display gems.ruby-china.com

# install bundler
gem install bundler

Nignx & SSL

$ sudo apt install nginx

在腾讯云可以申请免费的 SSL 证书,之后上传到 /etc/nginx/conf.d/ssl

/etc/nginx/site-available 新增对应 app 的 conf 配置文件,并软连接到 /etc/nginx/site-enabled

# /etc/nginx/site-available/app.conf
upstream myapp {
  # your app puma.socket file
  server unix:///home/deploy/app/your-app/shared/tmp/sockets/puma.sock;
}

server {
  listen 80;
  server_name your-domain.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2;

  server_name your-domain.com;
  root /home/deploy/app/your-app/current/public;
  index  index.html index.htm;

  access_log /home/deploy/app/your-app/shared/log/nginx.access.log;
  error_log /home/deploy/app/your-app/shared/log/nginx.error.log warn; # only record warn or higher level log

  # tencent cloud ssl cerification files location
  ssl_certificate /etc/nginx/conf.d/ssl/1_your-app_bundle.crt;
  ssl_certificate_key /etc/nginx/conf.d/ssl/2_your-app.key;
  ssl_session_timeout 5m;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
  ssl_prefer_server_ciphers on;

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;

    # If the file exists as a static file serve it directly without
    # running all the other rewrite tests on it
    if (-f $request_filename) {
      break;
    }

    # check for index.html for directory index
    # if it's there on the filesystem then rewrite
    # the url to add /index.html to the end of it
    # and then break to send it to the next config rules.
    if (-f $request_filename/index.html) {
      rewrite (.*) $1/index.html break;
    }

    # this is the meat of the rack page caching config
    # it adds .html to the end of the url and then checks
    # the filesystem for that file. If it exists, then we
    # rewrite the url to have explicit .html on the end
    # and then send it on its way to the next config rule.
    # if there is no file on the fs then it sets all the
    # necessary headers and proxies to our upstream pumas
    if (-f $request_filename.html) {
      rewrite (.*) $1.html break;
    }

    if (!-f $request_filename) {
      proxy_pass http://myapp;
      break;
    }
  }

  location ~* \.(ico|css|gif|jpe?g|png|js)(\?[0-9]+)?$ {
     expires max;
     break;
  }

  # Error pages
  # error_page 500 502 503 504 /500.html;
  location = /500.html {
    root /home/deploy/app/your-app/current/public;
  }

}

检查 nginx 配置并重新加载配置

# check configurations is ok
$ sudo nginx -t

$ sudo service nginx reload

Deployment

安装依赖

# mysql
$ sudo apt install -y build-essential
$ sudo apt install libmysqlclient-dev

# use nvm to install node
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.0/install.sh | bash
$ nvm -v
$ nvm install 12
$ node -v
$ nvm use default 12
$ nvm alias default node
# because execjs can't find nvm node,need to create a soft link, or add alias node=nodejs in .bashrc
# https://github.com/rails/execjs/issues/81
$ sudo ln -s $(which node) /usr/bin/node

# install yarn
# https://classic.yarnpkg.com/en/docs/install#debian-stable
$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
$ sudo apt update && sudo apt install --no-install-recommends yarn

使用 systemd 管理 puma https://github.com/puma/puma/blob/master/docs/systemd.md

如果使用 socket activation 方式来启动 puma,可以在 user level 创建 systemd,否则需要在 root level sudo 启动。因为我没有成功配置 socket activation,所以下面还是使用 sudo 来启动 puma

创建 service 文件 /etc/systemd/system/puma.service

[Unit]
Description=Puma HTTP Server
After=network.target

# Uncomment for socket activation (see below)
# Requires=puma.socket

[Service]
Type=simple

# always failed with watchdog, doesn't know why
# Type=notify
# WatchdogSec=15

Environment=RAILS_ENV=production

WorkingDirectory=/home/deploy/app/your-app/current

# your puma production configuration file location
ExecStart=/home/deploy/.rbenv/shims/bundle exec puma -C /home/deploy/app/your-app/shared/config/puma/production.rb

ExecReload=/home/deploy/.rbenv/shims/bundle exec pumactl -S /home/deploy/app/your-app/shared/tmp/pids/puma.state restart

ExecStop=/home/deploy/.rbenv/shims/bundle exec pumactl -S /home/deploy/app/your-app/shared/tmp/pids/puma.state stop

Restart=always

[Install]
WantedBy=multi-user.target

之后可以使用以下命令操作 puma

$ sudo systemctl daemon-reload

# enable
$ sudo systemctl enable puma.service

# disable
$ sudo systemctl disable puma.service

# start / restart / stop / status
$ sudo systemctl start puma.service

puma_production.rb sample

rails_env = 'production'
environment rails_env

application = "you-app"

app_path = "/home/deploy/app/#{application}"
shared_path = "#{app_path}/shared"
current_path = "#{app_path}/current"

directory "#{current_path}"


pidfile "#{shared_path}/tmp/pids/puma.pid"
state_path "#{shared_path}/tmp/pids/puma.state"

stdout_redirect "#{shared_path}/log/puma_access.log", "#{shared_path}/log/puma_error.log", true

bind "unix://#{shared_path}/tmp/sockets/puma.sock"

threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 }
threads threads_count, threads_count
workers 2

port ENV.fetch("PORT") { 3000 }

capistrano deploy.rb sample

# config valid for current version and patch releases of Capistrano
lock "~> 3.14.1"

set :application, "your-app"
set :repo_url, "your-git-repo-url"

ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp

set :deploy_to, "/home/deploy/app/your-app"

# Depending on your own configuration
append :linked_files, "config/database.yml", "config/master.key", "config/application.yml", "config/puma/production.rb"

# Depending on your own configuration
append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', '.bundle', 'public/system', 'public/uploads'

set :rbenv_type, :user
set :rbenv_ruby, File.read('.ruby-version').strip

set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec"
set :rbenv_map_bins, %w{rake gem bundle ruby rails}
set :rbenv_roles, :all # default value

set :migration_role, :app

# Whenever config
set :whenever_environment, fetch(:stage)
set :whenever_identifier, "#{fetch(:application)}_#{fetch(:stage)}"
set :whenever_variables, -> do
  "'environment=#{fetch :whenever_environment}" \
  "&rbenv_root=#{fetch :rbenv_path}'"
end

namespace :deploy do
  after :publishing, 'puma:restart'
  after :finishing, :cleanup
end

namespace :puma do
  task :restart do
    on roles :web do
      execute 'sudo systemctl restart puma.service'
    end
  end

  task :start do
    on roles :web do
      execute 'sudo systemctl start puma.service'
    end
  end

  task :stop do
    on roles :web do
      execute 'sudo systemctl stop puma.service'
    end
  end
end

whenever schedule.rb sample

if @rbenv_root
  job_type :rake,    %{cd :path && :environment_variable=:environment :rbenv_root/bin/rbenv exec bundle exec rake :task --silent :output}
  job_type :runner,  %{cd :path && :rbenv_root/bin/rbenv exec bundle exec rails runner -e :environment ':task' :output}
  job_type :script,  %{cd :path && :environment_variable=:environment :rbenv_root/bin/rbenv exec bundle exec script/:task :output}
end

set :output, "#{path}/log/cron.log"

logrotate sample

# sudo /etc/logrotate.d/your-app

/home/ubuntu/app/your-app/shared/log/*.log {
  daily
  missingok
  rotate 14
  compress
  delaycompress
  dateext
  notifempty
  copytruncate
  create 0640 ubuntu ubuntu
  sharedscripts
  postrotate
    kill -HUP `cat /home/deploy/app/your-app/shared/tmp/pids/puma.pid`
    invoke-rc.d nginx rotate >/dev/null 2>&1
  endscript
}

切换 npm 和 yarn 源 国内服务器每次部署会卡在 [4/4] Building fresh packages... 这一步,调整下面三个源可以解决

$ npm config set registry https://registry.npm.taobao.org
$ yarn config set registry https://registry.npm.taobao.org
$ yarn config set sass_binary_site http://cdn.npm.taobao.org/dist/node-sass -g 

sidekiq

在以下文件中增加对应配置

# Gemfile
gem 'sidekiq'
gem 'capistrano-sidekiq'

# Capfile
require 'capistrano/sidekiq'
install_plugin Capistrano::Sidekiq
install_plugin Capistrano::Sidekiq::Systemd

# deploy.rb
append :linked_files, 'config/sidekiq.yml' # add sidekiq.yml to shared path
set :sidekiq_config, -> { File.join(shared_path, 'config', 'sidekiq.yml') }
set :pty, false

# config/environments/production.rb
config.active_job.queue_adapter = :sidekiq

config/initializers/sidekiq.rb

Sidekiq.configure_server do |config|
  config.redis = { url: ENV['sidekiq_redis_url'] }
end

Sidekiq.configure_client do |config|
  config.redis = { url: ENV['sidekiq_redis_url'] }
end

systemd: sidekiq.service

写入 ~/.config/systemd/user/sidekiq.service

[Unit]
Description=sidekiq for your-app (production)
After=syslog.target network.target

[Service]
Type=simple
WorkingDirectory=/home/deploy/app/your-app/current

ExecStart=/home/deploy/.rbenv/shims/bundle exec sidekiq -e production

ExecReload=/bin/kill -TSTP $MAINPID

ExecStop=/bin/kill -TERM $MAINPID

StandardOutput=append:/home/deploy/app/your-app/shared/log/sidekiq.log
StandardError=append:/home/deploy/app/your-app/shared/log/sidekiq.error.log



RestartSec=1
Restart=on-failure

SyslogIdentifier=sidekiq

[Install]
WantedBy=default.target

开启 sidekiq.service

$ systemctl --user daemon-reload
$ systemctl --user enable sidekiq.service
$ systemctl --user start sidekiq.service
$ systemctl --user status sidekiq.service