Deploy Rails application với Nginx và Passenger

Trong bài viết ngày hôm nay, mình sẽ làm một tutorial đơn giản, hướng dẫn các bạn từng bước hoàn chỉnh để deploy một chiếc rails application lên một máy server. Để làm theo tutorial các bạn cần chuẩn bị 2 thứ :

  • Một thư mục sourecode của một rails app đơn giản.
  • Một máy tính để làm server.

1. Hiểu cơ bản về quá trình deploy một web application.

Deploy là quá trình triển khai phần mềm để đưa vào trạng thái sẵn sàng phục vụ khách hàng. Với một web application, sau khi chúng ta đã code và test cẩn thận trên máy local (môi trường development), quá trình deploy sản phẩm lên một môi trường khác sẽ diễn ra (có thể là staging hoặc production). Môi trường khác ở đây là một máy server - khác với máy local mà chúng ta dùng để code. Chúng khác nhau ở một điểm cơ bản: máy server là nơi mà tất cả người dùng sẽ truy cập đến nên nó thường có cấu hình tốt hơn, đủ để chịu được khối lượng request lớn từ phía người dùng. Và quá trình deploy lên server có thể hiểu đơn giản gồm các bước:

  • Copy thư mục code từ máy local lên máy server
  • Cài đặt các package và kết nối internet cho máy server , để máy có thể chạy được phần code .
  • Bật đúng port của máy server để có thể xử lý các HTTP request.
  • Gán domain trỏ đến máy server

Để có thể làm theo tutorial này, các bạn cần có một máy PC có cài hệ điều hành Ubuntu để làm máy server (nó nên khác với máy mà các bạn dùng để coding). Nếu không có máy thật, các bạn có thể thuê các máy remote server với dịch vụ của AWS EC2, Azure, , Google cloud computing

2. Cài đặt các package để có thể chạy rails application

Đầu tiên, để có thể chạy được rails app trên máy server , chúng ta phải cài đặt một số package cơ bản: Đầu tiên là cài ruby và một trình quản lý phiên bản ruby. Ở đây, mình chọn bộ đôi rvmruby 2.5.1. Cài rvm :

sudo gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
curl -sSL https://get.rvm.io | sudo bash -s stable
sudo usermod -a -G rvm `thaodp`

Trong hệ thống, mỗi khi ta dùng lệnh sudo với cờ secure_path, cần có một biến môi trường như thế này được setup set rvmsudo_secure_path=1 . Ta sẽ chạy câu lệnh dưới đây để cài đặtset rvmsudo_secure_path=1 ở những môi trường cần thiết:

if sudo grep -q secure_path /etc/sudoers; then sudo sh -c "echo export rvmsudo_secure_path=1 >> /etc/profile.d/rvm_secure_path.sh" && echo Environment variable installed; fi

Quan trọng, sau khi chạy xong lệnh trên, tắt terminal đi và đăng nhập lại server của bạn, hoặc nếu bạn đang đăng nhập remote từ một máy chủ khác, hãy cũng tạo một ssh session mới để đăng nhập lại server. Nếu bạn không làm vậy, rvm sẽ không chạy.

Sau đó cài ruby 2.5.1 thông qua rvm:

rvm install ruby 2.5.1
rvm --default use ruby 2.5.1

Cài rails:

gem install rails

Cài bundler :

gem install bundler --no-rdoc --no-ri

Vì mình dùng rails, nên mình sẽ cần cả nodejs package nữa . Vì asset pipeline của rails yêu cầu một môi trường Javascript runtime . Chúng ta sẽ cài nodejs :

sudo apt-get install -y nodejs &&
sudo ln -sf /usr/bin/nodejs /usr/local/bin/node

3. Cài đặt passenger và nginx

Hiểu đơn giản, Nginx là web-server (giống như Apache), Passenger là một app-server(giống như Puma) . Vậy web-server và app-server khác nhau như thế nào, chúng liên gì đến một rails app? Câu trả lời là:

  • Khi một người dùng thực hiện một hành động trên ứng dụng web của bạn, tức là họ sẽ gửi một request từ máy client của họ lên server. Web-server sẽ trực tiếp xử lý các request đó trước tiên, sau đó sẽ chuyển request đó cho Rails app. Trong trường hợp request đó chỉ cần trả lại các file tĩnh (như HTML, CSS, JS, ảnh,... ) thì web-server sẽ lưu sẵn các file tĩnh ấy và thực hiện trả về response ngay mà không cần đẩy request sang phía Rails app.

  • App-server là một phần của Rails app (app server mặc định của Rails là Puma , được tích hợp qua gem 'puma' ). App server tải code của app lên và giữ app đó trong bộ nhớ. Khi app server nhận được request từ web server, nó sẽ báo lại cho Rails app. Sau khi app xử lý xong request đó, app server sẽ gửi response lại cho web server (và cuối cùng là cho người dùng) . Vậy tại sao mình lựa chọn bộ đôi passenger và nginx cho quá trình deploy? Vì passenger được tích hợp bên trong module nginx , nên việc cài đặt và tìm hiểu các hướng dẫn để sử dụng bộ đôi này khá đơn giản. Bây giờ chúng ta sẽ cài đặt Nginx:

sudo apt update
sudo apt install nginx

Kiểm tra xem server nginx đã chạy chưa bằng lệnh sau:

systemctl status nginx

Nếu trong terminal hiện ra như sau, có nghĩa là web-server của bạn đang chạy:

Output
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since Fri 2018-04-20 16:08:19 UTC; 3 days ago
Docs: man:nginx(8)
Main PID: 2369 (nginx)
Tasks: 2 (limit: 1153)
CGroup: /system.slice/nginx.service
├─2369 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
└─2380 nginx: worker process

Bạn cũng có thể bật trình duyệt và truy cập vào ip ứng với máy chủ của bạn. Nếu web-server đang chạy, trình duyệt sẽ hiện như sau:

Tiếp tục, ta sẽ cài đặt passenger:

# Install PGP key and add HTTPS support for APT
sudo apt-get install -y dirmngr gnupg
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7
sudo apt-get install -y apt-transport-https ca-certificates

# Add APT repository
sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger trusty main > /etc/apt/sources.list.d/passenger.list'
sudo apt-get update

# Install Passenger + Nginx
sudo apt-get install -y nginx-extras passenger

Ta sẽ include module passenger + nginx tích hợp sẵn trong nginx , bằng cách sửa file /etc/nginx/nginx.conf ở dòng sau:

# include /etc/nginx/passenger.conf;

Ta bỏ dấu comment ở dòng đó đi:

include /etc/nginx/passenger.conf;

Nếu ta không tìm thấy dòng bên trên trong file /etc/nginx/nginx.conf , ta sẽ phải tự thêm nó vào trong block http như sau:

http {
    include /etc/nginx/passenger.conf;
    ...
}

Sau đó, ta restart lại nginx:

sudo service nginx restart

Sau khi cài đặt, chúng ta sẽ xác nhận lại quá trình cài đặt bằng lệnh sau:

sudo /usr/bin/passenger-config validate-install

Và các bước xác nhận sẽ hiện ra dấu tích passed như sau:

* Checking whether this Phusion Passenger install is in PATH... ✓
* Checking whether there are no other Phusion Passenger installations... ✓
....

Nếu có một số dấu passed không hiện ra, hãy làm theo chỉ dẫn trên terminal.

Cuối cùng, chúng ta sẽ check các process của Nginx và Passenger bằng lệnh:

sudo /usr/sbin/passenger-memory-stats

Và chúng ta sẽ thấy trên màn hình như sau:

---------- Nginx processes ----------
PID    PPID   VMSize   Private  Name
-------------------------------------
12443  4814   60.8 MB  0.2 MB   nginx: master process /usr/sbin/nginx
12538  12443  64.9 MB  5.0 MB   nginx: worker process
### Processes: 3
### Total private dirty RSS: 5.56 MB

----- Passenger processes ------
PID    VMSize    Private   Name
--------------------------------
12517  83.2 MB   0.6 MB    PassengerAgent watchdog
12520  266.0 MB  3.4 MB    PassengerAgent server
12531  149.5 MB  1.4 MB    PassengerAgent logger
...

Nếu bạn không thấy được dòng "Nginx processes" hoặc "Passenger processes" , có nghĩa là bạn nên kiểm tra lại quá trình cài đặt từ bước đầu tiên. Sau khi đã cài đặt hoàn chỉnh các package, chúng ta sẽ đi vào quá trình deploy phần code của rails app.

4. Cài đặt sourecode của Rails app

Bước đầu tiên, chúng ta cần copy thư mục sourecode của máy local lên máy server. Trong tutorial này, thì mình sử dụng máy server là 1 máy PC ở gần, sử dụng cùng hệ thống mạng với máy local nên mình có thể dễ dàng share sourecode từ máy local sang máy server. Tuy nhiên, với các máy remote server trong thực tế, cách phổ biến nhất để làm điều này là sử dụng các version source control như git + github để clone code về. Để chắc chắn đã đẩy hết các thay đổi của code ở máy local lên nhánh chính, ta thực hiện push code:

git push

Tiếp theo, mình sẽ đăng nhập máy server từ máy local bằng lệnh SSH:

ssh [email protected]

Thay adminuser bằng tên account có quyền trên máy server của bạn. Vì một vài lý do bảo mật, bạn nên tạo một user mới - là user duy nhất có quyền chạy ứng dụng web của bạn. User này là người duy nhất có quyền truy cập vào các thư mục liên quan đến việc cài đặt , vận hành và phần sourecode của ứng dụng. Đây là một ý tưởng tốt nhằm hạn chế các lỗ hổng bảo mật có thể xảy ra. Bạn nên đặt tên của user giống với tên của web application để thuận tiện cho việc ghi nhớ. Ở đây mình sẽ đặt nó là myappuser :

sudo adduser myappuser

Và hãy cài đặt SSH keys cho user này:

sudo mkdir -p ~myappuser/.ssh
touch $HOME/.ssh/authorized_keys
sudo sh -c "cat $HOME/.ssh/authorized_keys >> ~myappuser/.ssh/authorized_keys"
sudo chown -R myappuser: ~myappuser/.ssh
sudo chmod 700 ~myappuser/.ssh
sudo sh -c "chmod 600 ~myappuser/.ssh/*"

Bây giờ ta sẽ cài đặt git ở trên máy server :

sudo apt-get install -y git

Mình sẽ pull thư mục sourecode về máy server, và đặt nó vào một thư mục với đường dẫn là /var/www/APP_NAME . (Hãy đặt đường dẫn theo cách mà bạn muốn)

sudo mkdir -p /var/www/myapp
sudo chown myappuser: /var/www/myapp
cd /var/www/myapp
sudo -u myappuser -H git clone git://github.com/username/myapp.git code

Đăng nhập vào myappuser

sudo -u myappuser -H bash -l

Bây giờ, chúng ta cần cài đặt các dependencies của rails app . Các dependencies này là các gem, các thư viện được quản lý bởi Gemfile . Bạn có thể cài đặt nó bằng cách chạy lệnh dưới đây:

cd /var/www/myapp/code
bundle install --deployment --without development test

Cờ ----deployment --without development test ở đây nghĩa là bạn đang cài đặt các dependencies cho môi trường production . Rails app của bạn có thể còn phụ thuộc vào các services như PostgreSQL, Redis, ... Chúng không được cài thông qua lệnh bundle. Bạn cần tìm cách tự cài chúng để rails app có thể chạy được.

Bây giờ, chúng ta sẽ config các file database.ymlsecrets.yml . Các file này thường bị để trong .gitignore, nên khi pull code về sẽ không có chúng ở trên máy server. Với file database.yml bạn cần copy chúng từ máy local của bạn. Nó sẽ trông như thế này:

default: &default
  adapter: mysql2
  encoding: utf8
  pool: 5
  host: <%= ENV["DATABASE_HOSTNAME"] %>
  database: <%= ENV["DATABASE_NAME"] %>
  username: <%= ENV["DATABASE_USERNAME"] %>
  password: <%= ENV["DATABASE_PASSWORD"] %>
  socket: /var/run/mysqld/mysqld.sock

test:
  <<: *default
  database: test<%= ENV["TEST_ENV_NUMBER"] %>

production:
  <<: *default
  database: <%= ENV["DATABASE_NAME"] %>
  username: <%= ENV["DATABASE_USERNAME"] %>
  password: <%= ENV["DATABASE_PASSWORD"] %>

File secrets.yml sẽ như thế này:

production:
  secret_key_base: <%=ENV["SECRET_KEY_BASE"]%>

Vì đây là các file rất quan trọng và cần được bảo mật nên bạn cần phân quyền cho myappuser - trở thành user duy nhất có thể thay đổi và chỉnh sửa 2 file này:

chmod 700 config db
chmod 600 config/database.yml config/secrets.yml

Bây giờ bạn đã có thể, chạy database migrations và complile cho asset pipeline của rails:

bundle exec rake assets:precompile db:migrate RAILS_ENV=production

5. Cấu hình để Passenger + Nginx để chạy rails app

Sau khi bạn đã hoàn tất việc pull code rails app về, bạn cần phải chỉ cho Passenger + Nginx biết cách để có thể chạy rails app của bạn . Nếu trên máy server của bạn có nhiều phiên bản Ruby đang được cài đặt, bạn cần cho Passenger biết, nó nên dùng trình thông dịch ruby nào? Đầu tiên, chạy lệnh:

$ passenger-config about ruby-command

Trong terminal sẽ hiện ra giống như thế này:

passenger-config was invoked through the following Ruby interpreter:
  Command: /usr/local/rvm/gems/ruby-2.3.3/wrappers/ruby
  Version: ruby 2.3.3p85 (2015-02-26 revision 49769) [x86_64-linux]
  ...

Hãy lưu đường dẫn đằng sau key Command vào đâu đó:

 Command: /usr/local/rvm/gems/ruby-2.3.3/wrappers/ruby

Bây giờ, hãy thoát khỏi myappuser account và trở lại với admin acount

myappuser$ exit
admin$

Chúng ta cần tạo một file config và thiết lập một máy ảo trỏ đến rails app của chúng ta. Máy ảo này sẽ nói cho Passenger + Nginx biết app của chúng ta nằm ở đâu:

sudo nano /etc/nginx/sites-enabled/myapp.conf

Copy nội dung sau đây vào file nói trên:

server {
    listen 80;
    server_name yourserver.com;

    # Tell Nginx and Passenger where your app's 'public' directory is
    root /var/www/myapp/code/public;

    # Turn on Passenger
    passenger_enabled on;
    passenger_ruby /path-to-ruby;
}

Bạn hãy thay các dòng:

  • yourserver.com thay bằng domain trỏ đến máy chủ của bạn (có thể là địa chỉ ip của máy chủ)
  • /var/www/myapp/code/ thay bằng đường dẫn trỏ đến thư mục sourecode của rails app
  • /path-to-ruby thay bằng đường dẫn đã lưu ở sau key Command phía bên trên.

Xong rồi, bây giờ bạn có thể restart lại Nginx

sudo service nginx restart

Bây giờ bạn có thể truy cập vào domain máy chủ của bạn bằng trình duyệt , để xem web-app của bạn đã chạy chưa:

Đó là toàn bộ tutorial mà mình muốn trình bày.


References:

https://www.phusionpassenger.com/library/walkthroughs/deploy/ruby/ownserver/nginx/oss/trusty/deploy_app.html

https://www.javaworld.com/article/2077354/app-server-web-server-what-s-the-difference.html

https://medium.freecodecamp.org/lessons-learned-from-deploying-my-first-full-stack-web-application-34f94ec0a286