Deploy ứng dụng rails với capistrano và unicorn

Lời mở đầu

Thực ra trên viblo đã có nhiều bài hướng dẫn chi tiết việc deploy rails app lên môi trường server rồi. Bài này mình viết với mục đích để tổng hợp lại kiến thức và bổ sung kĩ thuật liên quan đến server cho bản thân.

Do không có điều kiện để thuê cloud server hay máy chủ, do đó mình sẽ build docker làm server. Nếu các bạn đã có cloud server như ec2 thì có thể bỏ qua phần build container và cài đặt ssh trong bài viết.

Let 's go

Cấu trúc server

Sơ đồ trên minh họa các công việc chúng ta cần phải cài đặt trên local và server:

  • Rails app: là một app được viết bằng ruby on rails
  • Unicorn: là một app server hỗ trợ rails - tương tự puma hay passenger. nó nằm giữa rails app và web server.
  • Nginx: là web server
  • Mysql: database cho rails app
  • Capistrano: tool tự động deploy từ local tới server remote

Build container ubuntu (remote server)

Trước khi làm build container các bạn có thể đọc qua các thao tác cơ bản khi làm việc với container tại đây

Mình sử dụng image ubuntu bản 16.04 để làm remote server nhé

$ docker images
REPOSITORY           TAG                 IMAGE ID            CREATED             SIZE
ubuntu               16.04               2a4cca5ac898        12 months ago       111MB

Build remote server bằng docker container. Mình đặt tên cho container là deploy_server:

$ sudo docker run --name deploy_server -it ubuntu:16.04 /bin/bash

Sau khi chạy xong lệnh này thì bạn đã vào trong container với quyền hạn cao nhất, việc bây giờ cần làm là cài đặt ssh để truy cập từ local đến host nhá.

Install SSH

Cài đặt ssh và vim bằng lệnh sau:

[email protected]:/# apt-get update
[email protected]:/# apt-get install -y vim openssh-server

Trước khi sửa file config ssh thì các bạn có thể tạo một bản backup nhé:

[email protected]:/# vim /etc/ssh/sshd_config

Các giá trị cần kiểm tra, nếu chưa có thêm vào cuối file:

MaxAuthTries 3
RSAAuthentication yes
PubkeyAuthentication yes
ChallengeResponseAuthentication no
PasswordAuthentication no
UsePAM no

Sau khi sửa xong bạn sẽ cần khởi động lại service

[email protected]:/# service ssh restart

Tạo thư mục .ssh để chứa publickey của máy mà chúng ta có thể SSH lên:

[email protected]:/# ~/.ssh
[email protected]:~# ssh-keygen -t rsa // sau đó enter để pass qua phần setup cũng được

Tạo file authorized_keys trong folder .ssh để chứa publickey với lệnh:

[email protected]:/# cd ~/.ssh
[email protected]:~/.ssh# touch authorized_keys
[email protected]:~/.ssh# chmod 600 authorized_keys
[email protected]:~/.ssh# ls
authorized_keys  id_rsa  id_rsa.pub

Bước tiếp theo lấy public key của local và add vào file authorized_keys của server:

  • Local: cat ~/.ssh/id_rsa.pub và copy đoạn SSH key

  • Server: vi ~/.ssh/authorized_keys và paste đoạn SSH của host vào

Thoát khỏi container bằng tổ hợp phím ctrl + d và ssh vào lại server. Tuy nhiên ta cần phải kiểm tra ip của deploy server là gì bằng câu lệnh sau:

$ sudo docker inspect deploy_server | grep IPAddress
"SecondaryIPAddresses": null,
            "IPAddress": "172.17.0.2",
                    "IPAddress": "172.17.0.2",

Sau khi thực hiện câu lệnh thì IP của deploy_server trên máy mình 172.17.0.2. SSH vào server bằng lệnh sau:

ssh [email protected]

Lưu ý:

  • Trước khi ssh còn phải kiểm tra xem container đã được start chưa bằng lệnh sau:
$ docker ps
1cb3a078d00b        ubuntu:16.04        "/bin/bash"         5 hours ago         Up 4 hours                              deploy_server
  • Trong trường hợp container chưa chạy, cần phải start container: docker start deploy_server
  • Truy cập lại deploy_server: docker exec -it deploy_server /bin/bash

Install Ruby on Rails

Cài đặt RVM theo trang trủ RVM.io

[email protected]:~# apt-get install -y curl
[email protected]:~# gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
[email protected]:~# curl -sSL https://get.rvm.io | bash -s stable

Để có thể sử dụng rvm bạn cần phải chạy lệnh: source /usr/local/rvm/scripts/rvm

Ngoài ra để các lần sau có thể tự động start mà ko cần gõ lệnh bạn chạy lệnh sau:

echo source /usr/local/rvm/scripts/rvm | tee -a ~/.bashrc

Tiếp theo để cài đặt các package cần thiết ta sẽ chạy lệnh sau . Sau đó tiền hành cài đặt ruby ở deploy_server mình sẽ cài phiên bản 2.3.3

[email protected]:~# rvm requirements
[email protected]:~# rvm install ruby-2.3.3

Cuối cùng ta sẽ cài đặt gem rails và bundle

[email protected]:~# gem install bundler
[email protected]:~# gem install rails

Install MySQL, NginX, Git, Node.JS and NPM

Trước tiên mình sẽ cài đặt mysql - server

apt-get install -y software-properties-common python-software-properties libmysqlclient-dev
add-apt-repository 'deb http://archive.ubuntu.com/ubuntu trusty universe'
apt-get install mysql-server

Sau khi cài đặt xong chạy lệnh start service mysql để start mysql. Sau đó bạn có thể kiểm tra lại service đã chạy chưa bằng lệnh sau:

mysql -uroot -p -e "show databases;"

Tiếp theo mình sẽ cài đặt git

apt-get install git

Để cài đặt nodejs và npm các bạn cần chạy lệnh sau:

apt-get install nodejs
apt-get install npm
ln -s `which nodejs` /usr/local/bin/node

Sau khi cài đặt xong bạn có thể kiểm tra lại xem đã thành công chưa: node -v hoặc nodejs -v

Cuối cùng là NginX:

apt-get install nginx

Nhớ start service service nginx start trước khi trước khi sử dụng nginx. Sau đó bạn có thể kiểm tra lại bằng cách gõ :

[email protected]:~# curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>

Lưu ý các bạn phải khởi động lại service của 3 thằng ssh, mysql, nginx sau mỗi lần start lại docker container nhé

service ssh start
service mysql start
service nginx start

Setup gem capistrano

Như mình đã giải thích ở trên capistrano sẽ đóng vai trò cầu nối giữa server và local. Để có thêm thông in về capistrano bạn có thể xem tại đây

Sửa gem file trong project như sau:

gem "unicorn"
group :development do
  gem "capistrano"
  gem "capistrano-rails"
  gem "capistrano3-unicorn"
  gem "capistrano-rvm"
end

Chạy bundle install để cài đặt các gem cần thiết. Sau đó gõ bundle exec cap install để init project

Trong file Capfile các bạn sửa như sau:

# Uncomment các require sau
require "capistrano/rvm"
require "capistrano/bundler"
require "capistrano/rails/assets"
require "capistrano/rails/migrations"
# Thêm require sau
require "capistrano3/unicorn"

Kiểm tra các xem unicorn đã chạy chưa Tiếp theo ta sẽ sửa file config/deploy.rb

lock "~> 3.11.0"

set :application, "prototype"
set :repo_url, "[email protected]:duongpham910/deploy_prototype.git"
set :bundle_binstubs, nil

# Default branch is :master
set :branch, "develop"

set :deploy_to, "/var/www/html/#{fetch(:application)}"

set :linked_files, fetch(:linked_files, [])
  .push("config/database.yml", "config/secrets.yml")
set :linked_dirs, fetch(:linked_dirs, [])
  .push("log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system", "vendor/bundle")

set :keep_releases, 5

after "deploy:publishing", "deploy:restart"

# Khởi động lại unicorn sau khi deploy
namespace :deploy do
  task :restart do
    invoke "unicorn:restart"
  end
end

Ý nghĩa của các params:

  • :application là tên ứng dụng sẽ deploy
  • :repo_url là Github repository URL
  • :deploy_to là thư mục sẽ chứa code deploy
  • :linked_files là các file dùng chung cho các bản deploy như secrets.yml, .env, database.yml, ...
  • :linked_dirs là các thư mục dùng chung cho các bản deploy
  • :keep_releases là số lượng bản deploy sẽ giữ lại. Tương đương với số lần bạn có thể rollback lại

Lưu ý: một số user có public key chứa kí tự không hợp lệ khi đặt tên file, dẫn đến có trường hợp capistrano tạo file git-ssh bị lỗi thì các bạn có sửa bằng cách thêm params sau: set :local_user, "duongph"

Tiếp theo mình sẽ sửa file conifg/deploy/staging để thiết lập cho môi trường staging

set :user, "root"
set :deploy_via, :remote_cache
set :conditionally_migrate, true
set :rails_env, "staging"

# Phần IP thì bạn thay thế cho phù hợp với IP của Docker container nhé
server "172.17.0.2", user: fetch(:user), port: fetch(:port), roles: %w(web app db)

Ý nghĩa của các params:

  • :user chính là user bạn sử dụng để ssh vào server
  • :rails_env: môi trường rails sử dụng để chạy rake task
  • :roles các các roles mà Capistrano sẽ sử dụng.

Lưu ý: nếu bạn có nhiều hơn một server, thì chỉ cần một server có chứa role db thôi (vì nhiều server cũng chỉ chung nhau một database và chỉ cần chạy migrate trên một server là đủ), ví dụ:

server "172.17.0.2", user: fetch(:user), port: fetch(:port), roles: %w(web app db)
server "172.17.0.3", user: fetch(:user), port: fetch(:port), roles: %w(web app)

Sau khi thiết lập xong các bạn có thể gõ bundle exec cap staging deploy:check để kiểm tra xem capistrano đã thiết lập liên kết với server chưa:

Capistrano sẽ báo lỗi ở phần deploy:check:linked_files do không tồn tại file database.ymlsecrets.yml.

00:04 deploy:check:linked_files
      ERROR linked file /var/www/html/prototype/shared/config/database.yml does not exist on 172.17.0.2

Do vậy chúng ta file ssh vào server để tạo 2 file con thiếu đồng thời thêm setting cho cả 2 file luôn: Lưu ý: file secret.yml chưa có key của môi trường staging thì bạn có thể chạy lệnh sau để thêm secret key: RAILS_ENV=staging bundle exec rake secret (còn phải đợi code đẩy lên mới chạy được)

Setup gem unicorn

Unicron là một ruby application servers được sử dụng điển hình cùng với một web server nginx. Khi người dùng request một page từ ứng dụng rails của bạn, nginx ủy quyền request cho application server. Tương tự với app server khác như là: puma hay passenger tuy nhiên unicorn cho phép việc deploy code lên server mà ko gây hiện tượng downtime - server ko phải dừng lại khi code mới được đẩy lên.

Đầu tiên ta sẽ tạo file config/unicorn/staging.rb và sửa nội dung như sau:

app_path = "/var/www/html/prototype/current"
working_directory app_path

pid "#{app_path}/tmp/pids/unicorn.pid"

stderr_path "#{app_path}/log/unicorn.err.log"
stdout_path "#{app_path}/log/unicorn.out.log"

worker_processes 3
timeout 30
preload_app true

listen "#{app_path}/tmp/sockets/unicorn.sock", backlog: 64

before_exec do |_|
  ENV["BUNDLE_GEMFILE"] = File.join(app_path, "Gemfile")
end

before_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!

  old_pid = "#{app_path}/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
    end
  end
end

after_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end

Ở đây bản chỉ cần lưu ý duy nhất là app_path = "/var/www/html/prototype/current" phải đúng đường dẫn khai báo bên phía capistrano

Tiếp theo trên server, chúng ta sẽ tạo file /etc/init.d/unicorn_deploy để start/restart/reload/stop ứng dụng Rails từ terminal nhé:

[email protected]:/etc/init.d# touch unicorn_deploy
[email protected]:/etc/init.d# vi unicorn_deploy

Sửa file unicorn_deploy như sau:

#!/bin/sh
set -u
set -e
# Example init script, this can be used with nginx, too,
# since nginx and unicorn accept the same signals
#[[ -s '/usr/local/rvm/scripts/rvm' ]] && source '/usr/local/rvm/scripts/rvm'

# Feel free to change any of the following variables for your app:
USER=root
GEM_HOME="/var/www/html/prototype/shared/bundle"
APP_ROOT="/var/www/html/prototype/current"
SET_PATH="export GEM_HOME=$GEM_HOME"

PID="$APP_ROOT/tmp/pids/unicorn.pid"
ENV="staging"
CMD="$SET_PATH; cd $APP_ROOT && bundle exec unicorn -D -E $ENV -c $APP_ROOT/config/unicorn/$ENV.rb"
old_pid="$PID.oldbin"

#cd $APP_ROOT || exit 1
$SET_PATH || exit 1

sig () {
  test -s "$PID" && kill -$1 `cat $PID`
}

oldsig () {
  test -s $old_pid && kill -$1 `cat $old_pid`
}

case $1 in
start)
  sig 0 && echo >&2 "Already running" && exit 0
  su - $USER -c "$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"
  su - $USER -c "$CMD"
  ;;
upgrade)
  sig USR2 && echo upgraded OK && exit 0
  echo >&2 "Couldn't upgrade, starting '$CMD' instead"
  su - $USER -c "$CMD"
  ;;
rotate)
  sig USR1 && echo rotated logs OK && exit 0
  echo >&2 "Couldn't rotate logs" && exit 1
  ;;
*)
  echo >&2 "Usage: $0 <start|stop|restart|upgrade|rotate|force-stop>"
  exit 1
  ;;
esac

Ở đoạn trên, bạn cần quan tâm các biến sau:

  • USER là user sẽ sử dụng để chạy ứng dụng, giống với user trong config/deploy/<stage>.rb
  • GEM_HOME là thư mục chứa Gem của ứng dụng, bạn sửa lại đường dẫn cho phù hợp
  • APP_ROOT là thư mục chứ ứng dụng, bạn sửa lại cho phù hợp
  • ENV là môi trường của ứng dụng, bạn sửa lại cho phù hợp

Sau khi lưu lại, bạn cần cấp quyền thực thi (executable) cho file đó với lệnh:

chmod +x /etc/init.d/unicorn_deploy

Cuối cùng ta sẽ sửa file config nginx /etc/nginx/sites-available/ để unicorn có thể connect được với nginx như sau:

[email protected]:/etc/nginx/sites-available# vi default

Sau đó, sửa file default với nội dung sau:

upstream prototype {
  server unix:/var/www/html/prototype/current/tmp/sockets/unicorn.sock fail_timeout=0;
}

server {
  listen 80 default deferred;
  # server_name example.com;
  root /var/www/html/prototype/current/public;

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  location ~ ^/(robots.txt|sitemap.xml.gz)/ {
    root /var/www/html/prototype/current/public;
  }

  try_files $uri/index.html $uri @prototype;
  location @prototype {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://prototype;
  }

  error_page 500 502 503 504 /500.html;
  client_max_body_size 4G;
  keepalive_timeout 10;
}

Sửa lại đường dẫn cho phù hợp với project của bạn và sau đó khởi động lại nginx bằng câu lệnh

service nginx restart

Trước khi đến bước cuối cùng còn một số việc phải làm như sau

  • Commit code và đồng bộ lên git đảm bảo nhánh deploy - cụ thể ở TH của mình là develop đã có code mới nhất.
  • Tạo db cho môi trường staging nhé: RAILS_ENV=staging bundle exec rake db:create.
  • Trên môi trường docker package tzdata không được cài đặt do đó gem tzinfo-data sẽ gây lỗi => giải pháp: apt-get update && apt-get install tzdata -y.

Cuối cùng là giây phút hồi hộp nhất là đẩy toàn bộ source code và config của bạn lên server :

bundle exec cap staging deploy

Quá trình deploy của bạn hoàn tất, còn một việc nhỏ mà bạn cần lưu ý mà mình đã nhắc ở trên là thêm secret key cho file secrets.yml. Sau đó khởi động lại unicorn để rails nhận setting nhé.

[email protected]:/var/www/html/prototype/shared/config# service unicorn_deploy stop   
[email protected]:/var/www/html/prototype/shared/config# service unicorn_deploy start

Kiểm tra lại thành quả của mình nào:

Tham Khảo

Bài viết của mình được tham khảo phần lớn từ anh leader của mình. Tuy nhiên mình đã cố gắng để bổ sung thêm những chi tiết cần lưu ý là những khó khăn mà mình gặp phải. Hy vọng bài viết này sẽ nâng cao kiến thức về mảng server của bạn:

Bài viết được tham khảo từ:

Link github project: