Build Docker, Entrypoint & wait-for-it.sh in ROR application?

Trong bài viết này chúng ta sẽ tìm hiểu 3 vấn đề:

  • Docker là gì? Tại sao nó lại thu hút vậy?
  • Các bước để xây dựng một Rails application sử dụng docker.
  • Add entrypoint và wait-for-it.sh

I. Docker là gì?

1.1. Docker là gì?

  • Docker là một công cụ được thiết kế để làm cho việc tạo, deploy và chạy ứng dụng dễ dàng sử dụng containers.
  • Containers cho phép developer đóng gói một ứng dụng với tất cả các phần cần thiết, như libraries và các depedencies khác, và đóng gói chúng lại trong một package.

1.2. Tại sao nó lại thu hút vậy?

Reason 1: Build once, Run anywhere

  • Làm cho việc build app dễ dàng hơn. => Tiết kiệm thời gian. Thay vì loay hoay trong việc setup môi trường để phù hợp với môi trường dự án, bạn có thể mất nửa ngày ngồi cài, rồi khi đi về nhà, bạn lại muốn làm thêm, nhưng máy của bạn lại không đáp ứng được môi trường dự án, bạn lại mất cả tối để setup. Ôi thật là một cực hình. 😢

Reason 2: Tăng hiệu suất, giảm kích thước của ứng dụng.

  • Theo một cách nào đó, docker hơi giống một máy ảo, nhưng không giống như máy ảo (hại não quá =))). Thay vì tạo ra toàn bộ hệ điều hành ảo, Docker cho phép các ứng dụng sử dụng cùng một nhân Linux như hệ thống mà chúng đang chạy và chỉ yêu cầu các ứng dụng được chuyển với những thứ chưa chạy trên máy chủ.

Reason 3: Docker is open source.

  • Điều này có nghĩa là bạn hay bất kỳ ai cũng có thể đóng góp cho Docker và mở rộng nó để đáp ứng nhu cầu riêng nếu bạn cần các tính năng bổ sung không có sẵn.

Tản mạn vậy thôi, giờ chúng ta cùng vào vấn để chính nào. 😃

2. Cài đặt Docker, Docker Compose vào ROR app

Để folow các bước dưới đây, bạn cần cài đặt Docker, Docker Compose

Trong bài này mình sử dụng Ubuntu 16.04

Step 1: Tạo một Rails application:

rails new rails-docker-app

Step 2: Tạo một Dockerfile ở thư mục root của project:

FROM ruby:2.6.5

# Install apt based dependencies
RUN apt-get update && apt-get install -y --no-install-recommends build-essential curl \
    && curl -sL https://deb.nodesource.com/setup_12.x | bash - \
    && apt-get install -y --no-install-recommends nodejs \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*
    
# Configure the main working directory.
ENV APP_DIR /app
RUN mkdir -p $APP_DIR
WORKDIR $APP_DIR

# Create a bundle folder in container
ENV BUNDLE_PATH=/bundle \
    BUNDLE_BIN=/bundle/bin \
    GEM_HOME=/bundle
ENV PATH="${BUNDLE_BIN}:${PATH}"

COPY . $APP_DIR

Mình sẽ giải thích từng dòng lệnh trong file trên

  1. FROM ruby:2.6.5: Sử dụng Ruby version 2.6.5

  2. RUN apt-get update && apt-get install -y --no-install-recommends build-essential curl \ && curl -sL https://deb.nodesource.com/setup_12.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && apt-get clean \ && rm -rf /var/lib/apt/lists/*: Chạy lệnh apt-get update để update apt repository, sau đó cài đặt các package cơ bản phục vụ cho việc code

  3. ENV APP_DIR /app: Set biến môi trường chỉ định làm việc với thư mục /app

  4. RUN mkdir -p $APP_DIR: Tạo một thư mục $APP_DIR

  5. WORKDIR $APP_DIR: Định nghĩa directory là thư mục $APP_DIR

  6. ENV BUNDLE_PATH=/bundle \ BUNDLE_BIN=/bundle/bin \ GEM_HOME=/bundle: Set biến môi trường BUNDLE_PATH, BUNDLE_BIN, GEM_HOME vào folder bundle để khai báo gem được cài đặt vào trong forder bundle này. Nếu không nó sẽ lưu mặc định trong /usr/local/bundle. điều này có tác dụng khi bạn thêm 1 gem mới nó sẽ check xem gem đó đã được cài đặt chưa nếu chưa nó sẽ chỉ cài đặt gem đó thôi.

  7. ENV PATH="${BUNDLE_BIN}:${PATH}": Mount folder bundle định nghĩa ở trên với path gem của máy tính.

  8. COPY . $APP_DIR: Copy folder local vào trong folder $APP_DIR của container

Step 3: Tạo một "docker-compose.yml" vào thư mục root của project:

Docker's definition of Compose is a great place to start.

Compose là một công cụ để xác định và chạy multi-container Docker applications. Mình sẽ tạo một file docker-compose.yml và giải thích từng dòng code trong comment luôn.

version: "3.7" # Sử dụng version 3.7

services:
  # Tạo một service db để chạy mysql
  db:
    image: mysql:5.7.28 # Tạo một image mysql version 5.7.28
    ports:
      - 3306:3306 # Map cổng 3306 ở ngoài máy local với cổng 3306 của mysql trong docker
    volumes: 
      - db-data:/var/lib/mysql # Tạo một thư mục db-data để lưu dữ liệu ngoài máy thật, sau đó map vào thư mục của container là /var/lib/mysql
    env_file: .env # Sử dụng file env để lưu biến môi trường
    restart: always # Để tránh downtime
    
  # Tạo một service app
  app:
    build:
      context: .
      dockerfile: ./Dockerfile
    command: scripts/wait-for-it.sh -t 120 db:3306 -- scripts/entrypoint # Note: Mình sẽ giải thích rõ hơn tại sao có dòng này ở bên dưới nha.
    volumes:
      - .:/app
      - bundle:/bundle # Map bundle folder in computer with bundle folder in container
    ports:
      - "3000:3000" # Map cổng 3000 trong container ra ngoài cổng 3000 của máy local.
    links:
      - db
    env_file: .env
    stdin_open: true # Chạy debug khi up docker-compose
    tty: true
volumes:
  bundle: 
  db-data:

Giải thích:

Volumes: Mọi dữ liệu trong container sẽ bị mất khi container bị down, đồng nghĩa với việc: dữ liệu trong container app và dữ liệu trong container db (bao gồm các record được tạo ra khi mình chạy seed, rails c, ...) sẽ bị mất.

Docker sinh ra volumes cho phép chúng ta lưu dữ liệu lại ở bên ngoài máy thật, sau đó map nó vào trong chỗ lưu dữ liệu của container.

Trong đoạn code trên bạn có thấy định nghĩa services db sử dụng mysql, để có thể sử dụng bạn cần phải tạo thêm:

  • file .env để lưu biến môi trường
# .env.example
DATABASE_HOSTNAME=db
DATABASE_USERNAME=your_username
DATABASE_PASSWORD=your_password
  • file database.yml
# database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  host: <%= ENV["DATABASE_HOST"] %>
  username: <%= ENV["DATABASE_USERNAME"] %>
  password: <%= ENV["DATABASE_PASSWORD"] %>

development:
  <<: *default
  database: rails-docker-demo_development

test:
  <<: *default
  database: rails-docker-demo_test

3. Entrypoint vs wait-for-it.sh

Trong code file docker-compose.yml có line này

command: scripts/wait-for-it.sh -t 120 db:3306 -- scripts/entrypoint

Nó có tác dụng gì nhỉ? Cùng tìm hiểu lý do vì sao sử dụng command này nhé.

Đầu tiên là scripts/wait-for-it.sh -t 120 db:3306

Điều hạn chế của docker là ta không biết khi nào một container sẵn sàng chạy. Docker support sử dụng depends_on để control thứ tự khởi động. Nhưng nó xác minh container đang chạy trước khi khởi động service.

Để đảm bảo không khởi động container của mình trước khi các dependency sẵn sàng được chấp nhận.

Ta thêm wait-for-it.sh vào folder scripts để điều khiển mọi thứ được khởi tạo trước khi khởi động container.

Bạn có thể đọc và tìm hiểu file script wait-for-it.sh tại đây

Thứ hai là scripts/entrypoint

File này dùng để chạy các các lệnh khi bạn chạy docker compose up. Bạn theo dõi các command trong code dưới nhé.

#entrypoint
#!/bin/bash
set -e

# Các gem đã cài đặt đã được mount từ volume vào folder /bundle trong container sẽ không phải install nữa, 
# chỉ khi có thay đổi mới thực hiện install.

bundle check || bundle install --binstubs="$BUNDLE_BIN"

# Check xem đã có database chưa, nếu chưa sẽ tạo database và migrate database
bundle exec rails db:prepare

rm -f tmp/pids/server.pid
# Chạy rails server
bundle exec rails s -b 0.0.0.0 -p 3000

exec "[email protected]"

Sau khi đã hoàn thành các bước trên, bạn chạy lệnh sau:

docker-compose build
docker-compose up

Tắt container:

docker-compose down

**Note: **

  • Bạn chỉ cần build image bằng lệnh docker-compose build khi có thay đổi trong Dockerfile
  • Trường hợp khác thì chỉ cần up để tạo bằng lệnh docker-compose updown để tắt bằng lệnh docker-compose down thôi nhé.

Một số lệnh hay dùng:

  1. docker ps -a: Hiển thị tất cả các containers
  2. docker start/stop container_id: Start/stop container
  3. docker attach container_id: Debug trong docker
  4. docker-compose run app /bin/bash: Chạy bash trong docker

Trên đây là cách để build một Rails app sử dụng docker. Hy vọng sẽ giúp ích cho bạn! Thankyou 😃 😃 😃