Running Ruby on Rails with Docker Compose
สวัสดีครับทุกท่าน ในบทความนี้เราจะมาสร้าง 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 ครับ