SOFTWARE DEVELOPMENT | 6 mins read

Running Ruby on Rails with Docker Compose

By Suea on 29 Jun 2020
sennalabs-blog-banner

สวัสดีครับทุกท่าน ในบทความนี้เราจะมาสร้าง Docker-Compose สำหรับ Ruby On Rails กันนะครับ เชื่อว่า Developer ส่วนใหญ่น่าพอจะรู้จัก Docker กันมั่งแล้ว ซึ่งเป็นสิ่งที่เข้ามาช่วยเราแก้ปัญหาในเรื่องของ Environment ในการพัฒนาซอฟต์แวร์ ทำให้ทุกคนในทีมสามารถพัฒนาซอฟต์แวร์บนแพลตฟอร์มที่เหมือนกันได้ หรือ สมาชิกใหม่ในทีมสามารถเริ่มรันโปรเจ็กได้โดยไม่ต้องลง Dependency ต่าง ๆ เพียงมีแค่ Docker และ Docker-Compose เท่านั้น

 

โดยตัวผู้เขียนเองได้มีโอกาสร่วมทำงานกับทีมที่ใช้ Ruby on Rails Framework ในการพัฒนา ซึ่งโดยปกติแล้วสิ่งที่จำเป็นต้องลงก่อนเริ่มพัฒนาจะมีดังนี้

 

  • Ruby

  • Postgres SQL

  • Redis

  • Yarn

 

อาจจะไม่ได้มากมายอะไร แต่... ตัวผู้เขียนเองไม่ได้พัฒนาเพียงแค่โปรเจ็กต์เดียว ซึ่งแต่ละโปรเจ็กต์ก็จะใช้ Dependency คนละเวอร์ชันกัน

 

ยกตัวอย่างเช่น

  • Project A ใช้ Ruby v2.5, Postgres v10 และ Redis v4

  • Project B ใช้ Ruby v2.6, Postgres v11 และ Redis v5

 

Docker compose จึงเข้ามามีบทบาทตรงนี้ โดยที่เราจะสร้าง docker-compose.yml ให้แต่ละโปรเจ็ก เพื่อที่เราจะได้ไม่ต้องคอยสลับเวอร์ชั่นซอฟต์แวร์ไปมา ซึ่งบางคนอาจมีปัญหาในการเปลี่ยนแปลงเวอร์ชั่นซอฟต์แวร์บางตัวรวมถึงตัวผู้เขียนเองเช่นกัน

 

เราจะสร้าง Service หลักๆขึ้นมา 4 ตัวดังนี้

  • Rails

  • Postgres

  • Redis

  • Sidekiq

 

แต่ในไฟล์ docker-compose.yml นั้น จะไม่ได้มีเพียงแค่ Services 4 ตัวนี้เท่านั้น แล้วจะมีอะไรบ้าง? มาดูกันเลย!

 

ตัวแรก

version: '3.4'
                
                services:
                  app: &app
                    build:
                      context: .
                      dockerfile: ./.dockerdev/Dockerfile
                      args:
                        RUBY_VERSION: '2.6.3'
                        PG_MAJOR: '11'
                        NODE_MAJOR: '11'
                        YARN_VERSION: '1.13.0'
                        BUNDLER_VERSION: '2.2.2'
                    image: project-a:1.0.0
                    tmpfs:
                      - /tmp

 

Service: app ตัวนี้จะทำหน้าที่ในการเก็บตัวแปรเวอร์ชั่นของ Dependency ที่เราจะใช้ในการสร้าง Image ตัวแปลพวกนี้ก็จะถูกเรียกใช้โดย Dockerfile ในขั้นตอนการ Build image

 

  backend: &backend
                    <<: *app
                    volumes:
                      - ./project-a:/app
                      - rails_cache:/app/tmp/cache
                      - bundle:/bundle
                      - node_modules:/app/node_modules
                      - packs:/app/public/packs
                      - .dockerdev/.psqlrc:/root/.psqlrc:ro
                    environment:
                      - NODE_ENV=development
                      - RAILS_ENV=${RAILS_ENV:-development}
                      - REDIS_URL=redis://redis:6379/
                      - DATABASE_URL=postgres://postgres:postgres@postgres:5432
                      - BOOTSNAP_CACHE_DIR=/bundle/bootsnap
                      - WEBPACKER_DEV_SERVER_HOST=webpacker
                      - WEB_CONCURRENCY=1
                      - HISTFILE=/app/log/.bash_history
                      - PSQL_HISTFILE=/app/log/.psql_history
                      - EDITOR=nano
                    depends_on:
                      - postgres
                      - redis

 

Service: backend ตัวนี้ก็ยังไม่ใช่ service หลักที่เราจะรันเหมือนกันครับ แต่ถือว่าเป็น base service ที่เอาไว้ให้ service ตัวอื่น extends ไปใข้งานได้

 

อธิบายเพิ่มเติม:

<<: *app หมายถึงเรา extends ออกมาจาก services ที่ชื่อ app ที่ระบุไว้ก่อนหน้านี้

 

ในส่วนของ volumes จะเห็นว่ามีสองแบบ

 

ได้แก่ ./project-a:/app และ bundle:/bundle

  • ./project-a:/app คือ การ mount ไฟล์จากเครื่องเราเข้าไปใน container สังเกตุที่ source path (./project-a) เป็นแบบ absolute path
  • bundle:/bundle คือ การสร้าง container volume สังเกตุที่ source path จะเป็นชื่อ volume ไปเลย ในส่วนนี้เดี๋ยวจะมีอธิบายเพิ่มเติมนะครับ

ในส่วนของ depends_on ได้อ้างอิงไปถึงอีกสอง service (ที่กำลังจะพูดถึง) นั่นหมายถึงหว่า เมื่อเรารัน service ตัวนี้ ตัวที่อยู่ภายใต้ depends_on ก็จะรันขึ้นมาด้วย

 

และแล้ว... ก็มาถึง services ตัวหลักๆของเรา

  rails:
                    <<: *backend
                    command: bundle exec rails server -b 0.0.0.0
                    ports:
                      - '3000:3000'
                
                  sidekiq:
                    <<: *backend
                    command: bundle exec sidekiq -C config/sidekiq.yml
                
                  postgres:
                    image: postgres:11.1
                    volumes:
                      - .psqlrc:/root/.psqlrc:ro
                      - postgres:/var/lib/postgresql/data
                      - ./log:/root/log:cached
                    environment:
                      - PSQL_HISTFILE=/root/log/.psql_history
                    ports:
                      - 5432
                
                  redis:
                    image: redis:4-alpine
                    volumes:
                      - redis:/data
                    ports:
                      - 6379

 

จะเห็นว่า rails และ sidekiq ได้ทำการ extends backend เหมือนกัน เพราะว่าจำเป็นต้องใข้ volume และ environment เดียวกัน

สาเหตุที่แยก service rails กับ sidekiq ก็เพื่อความง่ายในการเรียกใช้งานและจริง ๆ แล้วเราอาจไม่จำเป็นต้องใช้งาน sidekiq ตลอด

ตามมาด้วย service หลักอีกสองตัวคือ postgres และ redis สองตัวนี้ก็สร้างแบบปกติทั่วครับ

 

แต่ยังไม่หมดเพียงเท่านี้ เรายังขาดอีกส่วนที่จำเป็นไป นั่นก็คือ ...

volumes:
                  postgres:
                  redis:
                  bundle:
                  node_modules:
                  rails_cache:
                  packs:

 

การสร้าง volumes ให้กับ container นั่นเอง ซึ่ง volumes เหล่านี้สามารถใช้ร่วมกันในแต่ละ service ได้

เท่านี้เราก็จะได้ docker-compose.yml มาแล้ว มาชมหน้าตาไฟล์แบบเต็ม ๆ กันครับ

 

version: '3.4'
                
                services:
                  app: &app
                    build:
                      context: .
                      dockerfile: ./.dockerdev/Dockerfile
                      args:
                        RUBY_VERSION: '2.6.3'
                        PG_MAJOR: '11'
                        NODE_MAJOR: '11'
                        YARN_VERSION: '1.13.0'
                        BUNDLER_VERSION: '2.2.2'
                    image: project-a:1.0.0
                    tmpfs:
                      - /tmp
                  backend: &backend
                    <<: *app
                    volumes:
                      - ./project-a:/app
                      - rails_cache:/app/tmp/cache
                      - bundle:/bundle
                      - node_modules:/app/node_modules
                      - packs:/app/public/packs
                      - .dockerdev/.psqlrc:/root/.psqlrc:ro
                    environment:
                      - NODE_ENV=development
                      - RAILS_ENV=${RAILS_ENV:-development}
                      - REDIS_URL=redis://redis:6379/
                      - DATABASE_URL=postgres://postgres:postgres@postgres:5432
                      - BOOTSNAP_CACHE_DIR=/bundle/bootsnap
                      - WEBPACKER_DEV_SERVER_HOST=webpacker
                      - WEB_CONCURRENCY=1
                      - HISTFILE=/app/log/.bash_history
                      - PSQL_HISTFILE=/app/log/.psql_history
                      - EDITOR=nano
                    depends_on:
                      - postgres
                      - redis
                  rails:
                    <<: *backend
                    command: bundle exec rails server -b 0.0.0.0
                    ports:
                      - '3000:3000'
                
                  sidekiq:
                    <<: *backend
                    command: bundle exec sidekiq -C config/sidekiq.yml
                
                  postgres:
                    image: postgres:11.1
                    volumes:
                      - .psqlrc:/root/.psqlrc:ro
                      - postgres:/var/lib/postgresql/data
                      - ./log:/root/log:cached
                    environment:
                      - PSQL_HISTFILE=/root/log/.psql_history
                    ports:
                      - 5432
                
                  redis:
                    image: redis:4-alpine
                    volumes:
                      - redis:/data
                    ports:
                      - 6379
                volumes:
                  postgres:
                  redis:
                  bundle:
                  node_modules:
                  rails_cache:
                  packs:

 

วิธีใช้งาน

 

หากว่าเป็นครั้งแรกที่เริ่มใช้งาน เราก็ต้องทำการสร้าง image ขึ้นมาก่อนโดยการใช้คำสั่ง

 

docker-compose build

 

หลังจาก build เสร็จแล้ว เราก็จะได้ image ที่พร้อมใช้งาน (ถ้าตามตัวอย่าง image จะชื่อ project-a) ครวนี้เราก็จะทำการ migrate database โดยใช้คำสั่ง

docker-compose run rails rails db:schema:setup

 

"docker-compose run rails" หมายความว่าเราจะส่งคำสั่งเข้าไปใน service ที่ชื่อ rails โดยคำสั่งที่จะส่งไปก็คือ rails db:schema:setup

หลังจาก migrate database เรียบร้อยแล้ว web application ของเราก็พร้อมที่จะใช้งานแล้ว

เราจะสั่งรันเซิฟเวอร์ด้วยคำสั่ง

 

docker-compose up rails

 

เมื่อรันคำสั่งดังกล่าว docker ก็จะรัน service ขึ้นมา 3 ตัว ได้แก่ postgres, redis และ rails

เท่านี้เราก็สามารถเช้าใช้งาน web application ได้แล้ว ผ่าน http://localhost:3000 แต่ ... ถ้าใช้ Docker Toolbox จะต้องใช้ IP ของ docker machine โดยสามารถดู IP ได้โดยใช้คำสั่ง docker-machine ip

 

และขอทิ้งท้ายด้วยสิ่งที่หลายๆ คนอาจสงสัยว่าทำไมไม่มี ... สิ่งนั้นมันหายไปไหน ?

ก็คือ .... Dockerfile นั่นเอง

 

ARG RUBY_VERSION
                FROM ruby:$RUBY_VERSION
                
                ARG PG_MAJOR
                ARG NODE_MAJOR
                ARG BUNDLER_VERSION
                ARG YARN_VERSION
                
                # PostgreSQLUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
                  && echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list
                
                # NodeJS
                RUN curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash -
                
                # Yarn
                RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
                  && echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list
                
                # Install dependencies
                RUN printf 'nano' > /tmp/Aptfile
                RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
                  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
                    build-essential \
                    postgresql-client-$PG_MAJOR \
                    nodejs \
                    ca-certificates \
                    yarn=$YARN_VERSION-1 \
                    $(cat /tmp/Aptfile | xargs) && \
                    apt-get clean && \
                    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
                    truncate -s 0 /var/log/*log
                
                # Configure bundler and PATH
                ENV LANG=C.UTF-8 \
                  GEM_HOME=/bundle \
                  BUNDLE_JOBS=4 \
                  BUNDLE_RETRY=3
                ENV BUNDLE_PATH=$GEM_HOME
                ENV BUNDLE_APP_CONFIG=$BUNDLE_PATH \
                  BUNDLE_BIN=$BUNDLE_PATH/bin
                ENV PATH /app/bin:$BUNDLE_BIN:$PATH
                RUN unset BUNDLE_PATH && unset BUNDLE_BIN
                
                # Update GEM and install Bundle
                RUN gem update && \
                  gem install bundler:$BUNDLER_VERSION
                
                # Create a directory for the app code
                RUN mkdir -p /app
                
                WORKDIR /app

 

เนื่องจากมีเวลาจำกัด ดังนั้นในส่วนของ Dockerfile ผู้เขียนขออนุญาตอธิบายในบทความต่อไป หวังว่าบทความได้จะเป็นประโยชน์สำหรับผู้ที่ต้องการใช้งาน Ruby on Rails ด้วย Docker นะครับ และติดตามอ่านบทความดีๆ ที่น่าสนใจ ไม่ว่าจะเป็น Machine Learning, Startup, Design, Software Development และ Management ได้ทุกวันที่ Senna Labs Blog ครับ

Written By