SOFTWARE DEVELOPMENT | 5 mins read

Running Ruby on Rails with Docker Compose

By Suea on 06 Jan 2021
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

Please Tell Us Your Ideas

We will get back to you within 24 hours!