Deploy một ứng dụng rails đơn giản với Docker

Như chúng ta đã biết docker compose là công cụ tuyệt vời để deploy các ứng dụng Ruby Rails. Nó cho phép chúng ta có thể dễ dàng tách biệt các môi trường ruby, database hay thậm chí là Redis. Trong bài viết này chúng ta sẽ thực hiện các bước để deploy một ứng dụng rails lên heroku sử dụng docker bao gồm:

  • Setting một app rails trong docker compose
  • Run các tác vụ cần thiết như cài đặt gem, migration
  • Deploy lên heroku

1. Setting rails app với docker

Tạo một thư mục bằng cách chạy câu lệnh như sau:

$ mkdir -p ~/myapp/rails-compose
$ cd ~/myapp/rails-compose

Sau khi đã tạo xong thư mục, ta sẽ tạo một file Dockerfile với nội dung như sau:

FROM ruby:alpine

RUN apk add --update build-base postgresql-dev tzdata

Giải thích các câu lệnh trên:

  • FROM ruby:alpine câu lệnh này để ta sẽ lấy image Ruby mới nhất. Alpine linux là một base image nhỏ hơn
  • RUN apk add --update ...
  • build-base là gói chứa những tool cần thiết để complile từ source
  • postgresql-dev gói được sử dụng để cài đặt gem pg
  • tzdata được sử dụng bởi rails cho timezome Tiếp đến ta sẽ tham chiếu Dockerfile này từ một file docker-compose.yml
version: '3.6'

services:
  web:
    build: .

Khi sử dụng build . nó sẽ hiểu là có một Dockerfile nằm trong thư mục. Nó cũng sẽ sử dụng thư mục hiện tại khi build một image
Sau khi chúng ta tạo xong file này, có thể thử chạy

$ docker-compose build web

Nó sẽ pull image Ruby alpine và cài đặt các dependencies trên đó
Tiếp theo chúng ta cần init một project sử dụng câu lệnh rails new. Để làm điều đó ở bên trong container chúng ta cần mount những file local trong hệ thống vào bên trong container mà chúng ta setup.
Điều này sẽ cho phép ta có thể tạo được các file bên trong image web và chúng cũng sẽ xuất hiện ở dưới hệ thống local.
Chúng ta sẽ chỉnh sửa Dockerfile như sau:

FROM ruby:alpine

RUN apk add --update build-base postgresql-dev tzdata
RUN gem install rails -v '5.1.6'

Sau khi đã chỉnh sửa ta sẽ chạy lại lệnh

$ docker-compose build web

Nó sẽ build lại file image của ta với gem Rails bên trong, sau đó ta có thể chạy được lệnh rails new
Tiếp theo chúng ta cần thay đổi ile docker-compose.yml để thêm volumesworking directory

version: '3.6'

services:
  web:
    build: .
    volumes:
      - ./:/app
    working_dir: /app

Sau khi đã chỉnh sửa như trên, ta có thể chạy docker compose với câu lệnh rails new. Chúng ta sẽ tạo ra một application với database là Progres, bỏ qua javascript và các config khác bằng cách chạy câu lệnh

$ docker-compose run web rails new --database=progresql -J --skip-coffee

Sau khi chạy câu lệnh này thành công, thì thư mục của ta đã có đầy đủ file, folder cần thiết cho một rails app đơn giản và nó cũng gen ra cả Gemfile, Gemfile.lock
Khi sử dụng deploy bằng cách truyền thống, Bundler sẽ install các gem vào trong hệ thống file local. Do đó khi chạy câu lệnh như rails server nó sẽ load gem một ở local. Tuy nhiên khi ta sẽ dụng docker, docker-compose, gem cần phải được install vào trong image thay vì ở một nơi riêng biệt. Khi ta chạy rails new, nó sẽ install gem và sinh ra các file ở bên trong một file tạm thời. Để sửa và thực hiện như ta mong muốn ta sẽ sủa Dockerfile như sau

FROM ruby:alpine

RUN apk add --update build-base postgresql-dev tzdata
RUN gem install rails -v '5.1.6'

WORKDIR /app
ADD Gemfile Gemfile.lock /app/
RUN bundle install

Chúng ta vừa sửa để chỉ thêm GemfileGemfile.lock vào thư mục /app, sau đó sẽ chạy bundle install. Lý do chúng ta không cho cả thư mục vào image bởi vì là môt khi một file trong app thay đổi, docker sẽ hiểu là image này đang bị cũ, cần phải chạy lại thay vì sử dụng cache. Gemfile này ít thay đổi nên ta có thể thêm vào để tăng tốc đội build vì docker sẽ sử dụng cache nếu như không có gem nào được thêm vào cũng như thay đổi. Ta sẽ chạy lại

docker-compose build web

Tiếp đến, ta sẽ thay đổi file docker-compose.yml như sau

version: '3.6'

services:
  web:
    build: .
    volumes:
      - ./:/app
    working_dir: /app
    command: puma
    ports:
      - 3000:3000

Tại đây chúng ta sẽ add câu lệnh để start server puma trên cổng 3000. Ta sẽ chạy câu lệnh

docker-compose up web

Sau đó có thể vào đường dẫn http://localhost:3000 để xem kết quả.

2. Tạo model và migration

Tiếp theo chúng ta sẽ tạo một model đơn giản và migrate database
Tạo một model Post bằng cách chạy lệnh

$ docker-compose run --rm web rails g model Post content:text

Nó sẽ tạo một model Post và migrate bên trong container của chúng ta. Bởi vì ta đã định nghĩa volumes bên trong file docker-compose.yml , nó cũng sẽ nằm ở dưới hệ thống local.
Tiếp theo nếu ta chạy

$ docker-compose run --rm web rails db:migrate
rails aborted!
PG::ConnectionBad: could not connect to server: No such file or directory
    Is the server running locally and accepting
    connections on Unix domain socket "/tmp/.s.PGSQL.5432"?

Nó sẽ hiển thị lỗi chúng ta cần start progres database cho rails app để có thể kết nối đến được. Để làm ta sẽ thêm một image progres vào trong docker-compose.yml

version: '3.6'

services:
  web:
    build: .
    volumes:
      - ./:/app
    working_dir: /app
    command: puma
    ports:
      - 3000:3000
    depends_on:
      - db
    environment:
      DATABASE_URL: postgres://postgres@db
  db:
    image: postgres:10.3-alpine

Chúng ta vừa add thêm một service khác là db và sử dụng image progres. Chúng ta cũng thêm 2 phần nữa vào phần web đó là depends_on để chắc chắn rằng image progres đã được start và biến enviroment để cho rails app biết kết nối với nó như thế nào. Chúng ta cũng cần phải thay đổi file config/database.yml như sau

default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  url: <%= ENV['DATABASE_URL'] %>

development:
  database: app_development

test:
  database: app_test

Từ đây, ta có thể tạo database và run migrations

$ docker-compose run --rm web rails db:create db:migrate

Lệnh --rm để loại bỏ container được sử dụng để chạy câu lệnh này, để tiết kiệm được bộ nhớ dưới local.
Tiếp theo, ta sẽ tạo một dữ liệu với bảng posts vừa tạo bằng cách chạy

$ docker-compose run --rm web rails runner 'Post.create(content: "this is content of post")'

Thay đổi file config/routes.rb

Rails.application.routes.draw do
  root to: 'posts#index'
end

Tạo file controller

class PostsController < ApplicationController
  def index
    post = Post.order('RANDOM()').first

    render html: post.content
  end
end

Chúng ta sẽ chạy lại lệnh

docker-compose up web

Sau đó đến localhost:3000 để xem kết quả. Bài post ta vừa tạo sẽ xuất hiện trên màn hình

3. Deploy app lên Heroku

Bước cuối cùng chúng ta sẽ deploy app vừa tạo lên heroku. Ta sẽ thay đổi Dockerfile như sau

FROM ruby:alpine

RUN apk add --update build-base postgresql-dev tzdata
RUN gem install rails -v '5.1.6'

WORKDIR /app
ADD Gemfile Gemfile.lock /app/
RUN bundle install

ADD . .
CMD ["puma"]

Nó sẽ add những file local của ta(tất cả các file trong rails app) vào trong image và sau đó set câu lệnh default là puma. Do đó, khi ta run docker run nó sẽ tự động sử dụng puma.
Ta sẽ deploy Docker image lên Heroy bằng cách

$ git init // để tạo một repos trống

Sau đó chạy

$ heroku create

Ta cần phải login vào heroku và đẩy Docker image lên đó

$ heroku container:login
$ heroku container:push web
$ heroku container:release web

Chúng ta cần phải set up cho database progres bằng tay bằng cách chạy lệnh

$ heroku addons:create heroku-postgresql:hobby-dev

Tiếp đến ta cần phải thêm một vài biến môi trường cho nó

$ heroku config:set RAILS_ENV=production SECRET_KEY_BASE=supersecret RAILS_LOG_TO_STDOUT=true

Nó sẽ boot app của ta lên product (được định nghĩa trong config/environments/production.rb) và logs vào STDOUT cho phép ta nhìn được log khi sử dụng câu lệnh heroku logs
Tiếp đến ta sẽ migrate database trên heroku

$ heroku run rails db:migrate

Tạo dữ liệu mới của bảng posts trên heroku

$ heroku run rails runner 'Post.create(body: "This is content of post")'

Sau đó ta sẽ chạy và xem kết quả 😄

$ heroku open

4. Kết luận

Như vậy ta đã vừa tìm hiểu xong cách để deploy một ứng dụng rails đơn giản sử dụng docker. Hi vọng bài viết giúp ích cho mọi người, nếu có gì góp ý hay thảo luận hãy để lại bình luận phía dưới. (See you)


All Rights Reserved