# Web App Hosting
尝试过AWS Elastic Beanstalk和Azure Web App,前者出现的missing credentials直接把我劝退,因为还要我自己手动登陆EC2修改配置,那我还不如直接建一个EC2;后者则只支持Ruby 2.6,并且免费套餐不支持自定义域名。此外,这类一揽子服务使用的是Nginx,而我想用Caddy,没有为什么,用Caddy V2就是帅气!综合考量下来,我还是选择直接使用AWS EC2来部署我的应用。
一开始我选择手动git push
+ git pull
,不过稍微有些麻烦,于是决定部署一个Capistrano来自动化部署过程。
# 配置环境
# Ruby
安装Ruby 2.7.1时发现Ubuntu的软件库还停留在2.7.0,于是使用rbenv来安装Ruby,废话不多说,直接上命令。
# Fully update server packages
sudo apt-get update
sudo apt-get upgrade -y
# Choose Time zone=>Asia=>Shanghai
sudo dpkg-reconfigure tzdata
# Install rails dependencies, include libpq-dev if you are using postreSQL
sudo apt-get install -y build-essential git-core bison openssl libreadline6-dev curl zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-0 libsqlite3-dev sqlite3 autoconf libc6-dev libpcre3-dev curl libcurl4-nss-dev libxml2-dev libxslt-dev imagemagick nodejs libffi-dev libpq-dev
# Clone rbenv repo
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
# Setup rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
exec $SHELL
# Clone ruby-build plugin
git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
# Install Ruby 2.7.1; this might take some time
rbenv install 2.7.1
rbenv global 2.7.1
# Check ruby version
ruby -v
# Bundler
# Install Bundler
gem install bundler
rbenv rehash
# Yarn
由于系统自带软件源一般不包含我们所需要的Yarn,直接apt安装会安装错误的包,因此需要参照Yarn Documentation的各系统安装教程。我使用了Ubuntu的对应命令来安装Yarn。
# Configure package source
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
# Install yarn
sudo apt update && sudo apt install yarn
在依次运行上述命令之后,Ruby环境就配置完成了。
# Capistrano
Capistrano是一个Ruby Gem,其作用就是当你在本地运行部署命令时,在服务器执行相应脚本,来完成自动从git获取代码、编译、运行服务器等操作。
首先需要在Gemfile
中添加该Gem,只需要添加到development组即可。
group :development do
...
# Use capistrano for automated deployment
gem "capistrano", "~> 3.10", require: false
gem "capistrano-rails", "~> 1.5", require: false
gem "capistrano-yarn", require: false
gem "capistrano-rbenv", require: false
gem 'capistrano3-puma', require: false
end
由于我使用了Puma,因此使用了capistrano3-puma gem。
然后需要执行bundle命令来生成Capistrano的各个配置文件。
bundle exec cap install
默认的文件结构如下。
├── Capfile
├── config
│ ├── deploy
│ │ ├── production.rb
│ │ └── staging.rb
│ └── deploy.rb
└── lib
└── capistrano
└── tasks
# 配置Capfile
首先我在Capfile中添加/Uncomment了部署的各项步骤。
# Capfile
# Load DSL and set up stages
require "capistrano/setup"
# Include default deployment tasks
require "capistrano/deploy"
# Load the SCM plugin appropriate to your project:
#
# require "capistrano/scm/hg"
# install_plugin Capistrano::SCM::Hg
# or
# require "capistrano/scm/svn"
# install_plugin Capistrano::SCM::Svn
# or
require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git
# Include tasks from other gems included in your Gemfile
#
# For documentation on these, see for example:
#
# https://github.com/capistrano/rvm
# https://github.com/capistrano/rbenv
# https://github.com/capistrano/chruby
# https://github.com/capistrano/bundler
# https://github.com/capistrano/rails
# https://github.com/capistrano/passenger
#
# require "capistrano/rvm"
require 'capistrano/rails'
require "capistrano/rbenv"
# require "capistrano/chruby"
# require "capistrano/bundler"
# require "capistrano/rails/assets"
# require "capistrano/rails/migrations"
# require "capistrano/passenger"
require 'capistrano/puma'
install_plugin Capistrano::Puma
# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }
由于capistrano/rails
包含了bundler
、rails/assets
、rails/migrations
,因此只需要添加capistrano/rails
即可。而Yarn则被包含在了rails/assets
中(禁止套娃)。
# 配置deploy.rb
# config/deploy.rb
# config valid for current version and patch releases of Capistrano
lock "~> 3.14.0"
`ssh-add`
set :application, "<application name>"
set :repo_url, "<git repo ssh>"
...
# Default deploy_to directory is /var/www/my_app_name
set :deploy_to, "<deployment folder on server>"
...
# Default value for :linked_files is []
append :linked_files, "config/database.yml", "config/master.key", "config/application.yml"
# Default value for linked_dirs is []
append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system"
...
<application name>
指你的应用名称,<git repo ssh>
指代码Repo的SSH地址,<deployment folder on server>
指你想部署的服务器文件夹。
:linked_files
和:linked_dir
是指各环境共享的文件,存储在deploy_folder/shared
文件夹中,Capistrano会用symlink
的方式来保证每次部署都使用的是shared
文件夹中的这些文件。其中shared/config/database.yml
、shared/config/master.key
这两个文件需要手动创建,从本地复制即可。由于我使用的是figaro来存储环境变量,因此额外添加了shared/config/application.yml
。
# 配置SSH
需要为Capistrano配置两套SSH key,一套用于本地运行部署命令时连接到服务器,一套用于服务器连接git。
# 本地连接服务器
我只有一个production服务器,因此只配置config/deploy/production.rb
# config/deploy/production.rb
...
server "<server ip>", user: "ubuntu", roles: %w{app db web}, my_property: :my_value
...
set :ssh_options, {
keys: %w(~/.ssh/<aws ssh pem>),
forward_agent: true,
auth_methods: %w(publickey)
}
...
我直接使用了ubuntu用户来部署。<server ip>
是服务器IP,~/.ssh/<aws ssh pem>
是亚马逊提供的用于连接服务器的pem。
# 服务器连接Git
首先在服务器上运行keygen
ssh-keygen
一路回车使用默认设置创建~/.ssh/id-rsa.pub
。这是你的服务器的身份标识,将其倒入Github之后,Github就可以识别服务器,从而达到不需要密码使用git命令的目的。
cat ~/.ssh/id-rsa.pub
复制上述命令产生的key。
打开Github->设置->SSH Key,添加SSH Key,将我刚刚复制的key填入。
此时在服务器上运行git命令访问我的Repo就不需要密码了,可以使用git clone
来测试一下。此处需要注意remote url要使用ssh的格式,如git@github.com:user/repo.git
。
# Puma
接下来需要配置Puma。
# config/puma.rb
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port ENV.fetch("PORT") { 3000 }
# Specifies the `environment` that Puma will run in.
#
environment ENV.fetch("RAILS_ENV") { "development" }
# Specifies the `pidfile` that Puma will use.
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together,
# the concurrency of the application would be max `threads` * `workers.`
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
workers ENV.fetch("WEB_CONCURRENCY") { 2 } # <------ uncomment this line
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
preload_app! # <------ uncomment this line
# Allow Puma to be restarted by the `Rails restart` command.
plugin :tmp_restart
以上是一个默认的Production Puma配置示例,将其保存后,使用cap
命令将其上传到shared
文件夹中备用。
cap production puma:config
# 暂时解决每次Deploy时Puma不能正常重启
这应当是一个Puma的Bug,最早出现在3.8.2版本,短暂的修复后,近期再次出现。在部署之后,不能访问站点,查看shared/log/puma_error.log
,发现Puma重启出错。
一个Workaround是修改puma:restart
任务,手动使其puma:stop
,然后再puma:start
。
在lib/capistrano/tasks
下建立新文件,示例如下。
# lib/capistrano/tasks/restart_puma.rake
namespace :puma do
Rake::Task[:restart].clear_actions
desc 'Overwritten puma:restart task'
task :restart do
puts 'Overwriting puma:restart to ensure that puma is running. Effectively, we are just starting Puma.'
puts 'A solution to this should be found.'
invoke 'puma:stop'
invoke 'puma:start'
end
end
然后进行部署,可以看到以下输出:
Overwriting puma:restart to ensure that puma is running. Effectively, we are just starting Puma.
A solution to this should be found.
01:44 puma:stop
01 $HOME/.rbenv/bin/rbenv exec bundle exec pumactl -S /home/ubuntu/srv…
01 Command stop sent success
✔ 01 ubuntu@3.34.127.18 0.875s
01:45 puma:start
using conf file /home/ubuntu/srv/heroes-of-ezantoh/shared/puma.rb
01 $HOME/.rbenv/bin/rbenv exec bundle exec puma -C /home/ubuntu/srv/he…
01 Puma starting in single mode...
01 * Version 4.3.5 (ruby 2.7.1-p83), codename: Mysterious Traveller
01 * Min threads: 0, max threads: 16
01 * Environment: production
01 * Daemonizing...
✔ 01 ubuntu@3.34.127.18 1.051s
此时Puma已经正常重启。但这一Workaround的缺陷就是不是无缝衔接,会有downtime。我们只能静待更新了。
# 检查配置
使用下面的命令让Capistrano检测配置是否正确。
cap production deploy:check
如果出现问题,请用你的technical sophistication
来解决。
# Caddy
Caddy的配置相对简单,首先参看我过去的博文安装Caddy V2。
然后修改/新建/etc/caddy/Caddyfile
。
# Caddyfile
yourdomain.com {
root * <deployment folder>/current/public/
route {
file_server /packs*
reverse_proxy unix/<deployment folder>/shared/tmp/sockets/puma.sock {
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Port {server_port}
header_up X-Forwarded-Proto {scheme}
}
}
}
<deployment folder>
是刚刚在deploy.rb
设置的部署文件夹。思路是使用route
命令来保证能优先访问到JavaScript和CSS等资源文件,然后在不是访问packs
文件夹下的文件的情况下,将请求转发到puma。
Note: 需要提醒的是Caddy V2的unix连接格式是这样的:
unix//foder/server.sock
,而并不是以unix:///
开头。
由于我用了Webpacker来接管JavaScript和CSS,因此我只使用file_server命令serve了packs
文件夹。
然后使用systemctl
三件套来启动Caddy服务。
sudo systemctl daemon-reload # 重新加载修改过的service文件
sudo systemctl enable aria2 # 开启自启动
sudo systemctl start aria2 # 启动服务
# 部署
最后的部署过程比较简单。
cap production deploy
目前没有解决的问题是从第二次部署开始,每次部署之后需要手动启动Puma。
cap production puma:start
其余已经非常完美了。