Dockerize Rails App
終於補到這篇了!需要 Dockerize 的原因很簡單,因為本地環境被我自己搞爆了😀 簡單來說就是我七八月時開發的專案,隔了四個月之後再回來繼續支援時,發現整個環境出一堆問題(基本上都是 M1 chips 相關的),不管怎麼改環境設定,下完指令之後永遠都會 crash,修到最後我也懶得繼續找原因了,想說剛學過 Docker,其他資深的同事們也有這樣搞過,就想說不然我也來試試看吧:)
這個專案用的 Ruby 版本是 2.6.9(所以才會在 M1 的電腦上充滿問題,最一開始在安裝 2.6.9 版的 Ruby 時就已經動過一些手腳了:在 make binary 出問題的話可以先下 CFLAGS="-Wno-error=implicit-function-declaration" rvm install x.x.x
再安裝 by 老闆)還會需要用到 Redis, PostgresQL(DB), Webpacker,就都是一些開發 Rails 時所需最基本的配置,並在容器化後使用 docker-compose 來協調這些配置。
Docker-compose 簡介
根據官網的介紹,docker-compose 是一個用於管理、執行多容器 Docker Application 的工具。使用 docker-compose 時需要用 YAML 檔來定義 application 的各個 services,接下來即可透過一個指令、從配置中創建並啟動所有服務。這樣做的好處是可以同時定義、協調所有服務,可以避免在個別開啟容器時還要做額外的設定,提高開發的效率。
基本上在設定 docker-compose 時,需要先有各個 service container 要用的 image,不論是自己寫的 Dockerfile,或者是從 docker hub 上 pull 下來的 image 都可以。接下來就是 docker-compose.yml
的撰寫,需要先對整個應用程式做全域的設定(如使用的 docker-compose 版本、要有哪些 networks 等),接著再對個別的 services 做細節設定(選用哪裡的 image、port mapping、專案的環境變數…等)
Docker-compose Configurations
在 docker-compose.yml
的開頭需要先對整個 application 做最基本的設定,但因為這邊只是開發環境而已,因此就只有簡單做 Networks & Volumes 的設定而已,主要就是開兩個 Networks(development
& test
) 與四個 Volumes(db_data
, gem_cache
, shared_data
& packs
)。
Basic Setups
- Version: 指定要使用的 docker-compose 版本,目前看到大多數的教學都是用第三版
- Networks: 在建起 application 之後,docker-compose 會把所有容器都丟到一個 default network 中,而在同個 network 裡的容器都互相 reachable。如果不想使用預設的 default network 的話也能自己另外宣告,並創造更複雜的 network topology。基本上可以透過這樣的設定達到區隔環境的效果,這也是為什麼這個專案的 networks 設置要分成 development 與 test 的原因
- Volumes: 統一宣告所有 images 會用到的 volumes,因為有些 services 可能會需要共用 volumes(像是這個專案裡的
shared_data
volume 就是拿來讓所有 containers 共用的 volume)。主要的功用是在開發的時候可以同步將本地端修改的內容,mapping 到容器裡的專案,在開發上比較節省時間。Mapping 的格式:
外界:容器
Services Configurations
跟上面提到的配置一樣,再加上需要測試用的環境之後,我們在 docker-compose 裡總共會需要以下五個服務: redis
, db
(postgres), app
, test
, webpacker
Redis
- Docker-compose service 設定
- image: 可以直接選用 docker hub 上的 redis image
- command: 當這個服務開始運行時要執行的指令,aka 執行 redis server 的指令
redis-server
- networks: 這邊因為開發與測試環境都會用到 redis,所以兩個 network 都要放進來
- volumes: 這邊只需要
shared_data
AppName_redis: image: redis:6.0-alpine command: redis-server networks: - development - test volumes: - shared_data:/var/shared/redis
- Rails App 設定
- 需要將 app 內設定的 redis url 改成 docker-compose 裡指定的 service name 格式:
"redis://AppName_redis:6379/1"
(development & test 兩個環境下都需要改) Reference
- 需要將 app 內設定的 redis url 改成 docker-compose 裡指定的 service name 格式:
DB(PostgresQL)
- Docker-compose service 設定
- image: 這邊也是直接選用 docker hub 上的 postgres container
- volumes: 除了所有 service 共用的
shared_data
外,還需要指定一個用來儲存資料庫資訊的 volume(db_data
) - networks: 這邊也是兩個環境都會需要 db,因此兩個 network 都需要放
- environment: 用來設定這個 container 的環境變數,這邊需要設定的是資料庫的帳號密碼,而官方 image 預設的帳號為
postgres
密碼為password
;因為這邊只是測試環境,所以用預設的沒關係,但如果上 staging 或 production 這種正式的環境時,就需要另外用 CI 工具做設定。 - ports: 做 port mapping 用,讓資料庫可以被外界讀取
AppName_db: image: postgres:14-alpine container_name: AppName_db volumes: - db_data:/var/lib/postgresql/data - shared_data:/var/shared networks: - development - test environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: password ports: - 5432:5432
- Rails App 設定:
config/database.yml
- 需要將整個 app 要連結的 db 改成 docker-compose 上的 service,並且設定帳號密碼
- host:
AppName_db
- username:
postgres
-> image default setting - password:
password
-> image default setting
- 開啟整個服務後要記得先下
bundle exec rake db:create
來創建會用到的 db,否則資料庫的內容會長在一個很奇怪的位置,後續如果要用 rake 的指令操作資料庫會沒辦法讀取到正確的資料
App
- Dockerfile for Rails App
- 2 base images
node:14-alpine
,ruby:2.6.9-alpine
: 這邊會用到兩層 base image 是因為專案裡的 node 版本是 14,不過如果只用apk add
去下載 node 的話都會載到最新的 16 版,因此最後決定載兩層 base image,並且把帶 14 版的設定複製到 ruby image 裡,node 的部分就能正常運作了 - Env variables
- Copy entrypoint scripts and grant execution permission
- Copy everything from node image
- Install all dependencies(not including node)
- 這邊要避開下載 node,以免把 14 版的設定覆蓋掉
FROM node:14-alpine as node RUN apk add --update --no-cache python2 && ln -sf python2 /usr/bin/python FROM ruby:2.6.9-alpine ENV APP_PATH /var/app ENV BUNDLE_VERSION 2.2.33 ENV BUNDLE_PATH /usr/local/bundle/gems ENV TMP_PATH /tmp/ ENV RAILS_LOG_TO_STDOUT true ENV RAILS_PORT 3000 # copy entrypoint scripts and grant execution permissions COPY ./dev-docker-entrypoint.sh /usr/local/bin/dev-entrypoint.sh COPY ./test-docker-entrypoint.sh /usr/local/bin/test-entrypoint.sh COPY --from=node . . RUN chmod +x /usr/local/bin/dev-entrypoint.sh && chmod +x /usr/local/bin/test-entrypoint.sh # install dependencies for application RUN apk -U add --no-cache \ build-base \ git \ postgresql-dev \ postgresql-client \ libxml2-dev \ libidn-dev \ libxslt-dev \ yarn \ imagemagick6 \ imagemagick6-c++ \ imagemagick6-dev \ imagemagick6-libs \ tzdata \ less \ curl \ bash \ && rm -rf /var/cache/apk/* \ && mkdir -p $APP_PATH RUN gem install bundler --version "$BUNDLE_VERSION" \ && rm -rf $GEM_HOME/cache/* RUN yarn install --check-file # navigate to app directory WORKDIR $APP_PATH EXPOSE $RAILS_PORT ENTRYPOINT [ "bundle", "exec" ]
- 2 base images
- Docker-compose service 設定
- image: 使用上述的 Dockerfile 建出的 image,跟上面幾個服務不一樣,要使用
build
來指定建立 image 的資料夾還有 Dockerfile 的位置 - volumes: 除了共用的
shared_data
外,還有整個專案的 mapping,需要對應到剛剛 Dockerfile 裡定義的$APP_PATH
上,藉由 Docker bind mounts 達到 hot reloading 的效果;另外還有gem_cache
的 volume,讓 dependencies 可以被清理和重建而不會干擾 app 的其他部分 - stdin_open: 讓容器的標準輸入保持打開
- tty: 將 Docker 分配一個虛擬終端(pseudo-tty)並綁定到容器的標準輸入上,讓我們可以使用 byebug 來進行 debug
- entrypoint: 設定當對 docker-compose 裡的 container 下指令時的進入點
- command: 當啟動容器後要執行的指令
- env_file: 環境設定檔
- environment: 環境變數,可以被寫在環境設定檔裡,這邊會分開寫是因為公司的專案裡已經有預設的環境設定檔了,為了不洗掉原本的設定才另外寫在這個項目下。
- depends_on: 開啟這個服務前需要其他哪些服務的支援
AppName_app: build: context: . dockerfile: Dockerfile.dev container_name: AppName_app volumes: - .:/var/app - shared_data:/var/shared - gem_cache:/usr/local/bundle/gems networks: - development ports: - 3000:3000 stdin_open: true tty: true entrypoint: dev-entrypoint.sh command: - rails server -p 3000 -b 0.0.0.0 env_file: .env.example environment: RAILS_ENV: development WEBPACKER_DEV_SERVER_HOST: webpacker depends_on: - AppName_db - webpacker
- image: 使用上述的 Dockerfile 建出的 image,跟上面幾個服務不一樣,要使用
- ActionMailer 相關設定
- 如果會用到 ActionMailer 的話需要把 default_url_options 改成 docker-compose 上指定的 ip address,
config/environments/development.rb
:config.action_mailer.default_url_options = { host: '0.0.0.0:3000' }
- 如果會用到 ActionMailer 的話需要把 default_url_options 改成 docker-compose 上指定的 ip address,
Test
- Docker-compose service 設定
- image: 同樣使用 app 的 image
- volumes: 與 app 的設定相同
AppName_test:
image: AppName_AppName_app
container_name: AppName_test
volumes:
- .:/var/app
- shared_data:/var/shared
- gem_cache:/usr/local/bundle/gems
networks:
- test
ports:
- 3001:3000
stdin_open: true
tty: true
entrypoint: test-entrypoint.sh
command: ["rails", "-v"]
environment:
RAILS_ENV: test
WEBPACKER_DEV_SERVER_HOST: webpacker
depends_on:
- AppName_db
- webpacker
- 注意事項
test
service 在建立完之後本來就會死掉- 執行測試:
docker-compose run --rm AppName_test rspec
Webpacker
最後就是被搞得半死ㄉ webpacker 了!終於要結束了!!!😀🔪
- Rails App’s Webpack settings:
config/webpacker.yml
- 基本上就是需要把 webpacker 設定成 docker-compose 裡配置的 webpacker 路徑
- dev_server:
- host:
webpacker
(→ webpacker container name) - public:
0.0.0.0:3035
(→0.0.0.0
為 rails host)
- host:
- Webpack container settings:
- 使用與 rails app container 相同的 Dockerfile、相同的 volume
Node.js
設定 v14:Dockerfile.dev
: 用兩層 base imageFROM node:14-alpine as node
- 下載 python2
RUN apk add --update --no-cache python2 && ln -sf python2 /usr/bin/python
FROM ruby:2.6.9-alpine
- 把 node 那邊的設定複製過來:
COPY --from=node . .
- 把 node 那邊的設定複製過來:
- 要先執行
yarn install
等指令,生成 lock file - Heap out of memory error: ref
- Set environment variable:
- NODE_OPTIONS=--max_old_space_size=4096
webpacker:
build:
context: .
dockerfile: Dockerfile.dev
command: ruby bin/webpack-dev-server
volumes:
- shared_data:/var/shared
- gem_cache:/usr/local/bundle/gems
- .:/var/app
environment:
- NODE_OPTIONS=--max_old_space_size=4096
- NODE_ENV=development
- RAILS_ENV=development
- WEBPACKER_DEV_SERVER_HOST=0.0.0.0
ports:
- "3035:3035"
networks:
- development
所有改動過的檔案列表
config/cable.yml
:url: "redis://AppName_redis:6379/1"
config/database.yml
:- host: AppName_db(db service name)
- username: postgres
- password: postgres
- database: postgres
config/environments/development.rb
:config.action_mailer.default_url_options = { host: '0.0.0.0:3000' }
config/settings.yml
:host: localhost:3000
config/webpacker.yml
:host: webpacker
public: 0.0.0.0:3035
好ㄌ….大概就是這樣,寫這篇真的豪累,但是寫 k8s 那兩篇好像會更累🥲?(哭ㄌ
Reference- Dockerfile base: Rails 6 development with Docker and Docker Compose
- Webpacker container: rails開發環境容器化實戰指南