要將 Ruby on Rails 透過容器的方式部署,我們就需要讓我們的專案可以被容器化。這篇文章我們會採用最簡單的方式進行容器化,扣除掉有使用特殊的 Ruby Gem 的情況下,大多能夠透過這種方式完成容器化。
在開始之前,我會建議先看過如何在幾分鐘內容器化 Rails 專案這篇文章,裡面提供了一個更容易的方式來容器化你的專案。
Dockerfile 撰寫
目前最主流的容器化方式就是透過撰寫 Dockerfile 來實現這件事情,同時大部分容器的打包(Package)工具也都能夠支援這個格式。
撰寫 Dockerfile 的基本原則也不難,我們的目標就是將「安裝環境」的所有操作寫到 Dockerfile 裡面,並且讓打包工具幫我們轉換成鏡像(Image)用於部署。
建立 Dockerfile
首先,我們先在 Rails 專案下新增一個叫做 Dockerfile
的檔案,並且將以下內容放到裡面。
1FROM ruby:2.7.5
這裡我們先使用 FROM
語法指定基礎的鏡像,要使用哪個版本的 Ruby 具體依照你目前的 Rails 環境為前提設定,這邊預設以目前新專案比較主流的 Ruby 2.7 為例子。
安裝 RubyGems
接下來我們要將 Ruby Gem 安裝進去,在使用 Docker 時前面的指令變化會影響後面的指令造成重新執行,因此我們會先將 Gemfile
和 Gemfile.lock
使用 COPY
指令複製到容器中,讓我們可以先使用 RUN
指令執行 bundle install
來安裝 Ruby Gem。
1FROM ruby:2.7.5
2
3RUN mkdir -p /src
4COPY Gemfile Gemfile.lock /src
5
6WORKDIR /src
7RUN gem install bundler:2.2.32 \
8 && bundle config --local deployment 'true' \
9 && bundle config --local frozen 'true' \
10 && bundle config --local without 'development test' \
11 && bundle install -j "$(getconf _NPROCESSORS_ONLN)"
在這個範例中我們使用了正式環境的設定,因此會透過 bundle config
告訴 Bundler 要使用「部署模式」並且「凍結」來避免在安裝或執行的時候有我們預期以外的 Gem 被安裝進去。
導入專案
安裝完 Ruby Gem 之後,我們還需要將專案的原始碼複製到容器中才能夠執行。
1FROM ruby:2.7.4
2
3RUN mkdir -p /src
4COPY Gemfile Gemfile.lock /src
5
6WORKDIR /src
7RUN gem install bundler:2.2.32 \
8 && bundle config --local deployment 'true' \
9 && bundle config --local frozen 'true' \
10 && bundle config --local without 'development test' \
11 && bundle install -j "$(getconf _NPROCESSORS_ONLN)"
12
13COPY . /src
這個階段我們基本上就能夠直接用 bundle exec rails server
將伺服器啟動起來,然而我們還需要確保 RAILS_ENV
為 production
來避免載入不必要的 Gem,因此需要再加入一些額外的調整進行設定。
1FROM ruby:2.7.4
2
3RUN mkdir -p /src
4COPY Gemfile Gemfile.lock /src
5
6WORKDIR /src
7RUN gem install bundler:2.2.32 \
8 && bundle config --local deployment 'true' \
9 && bundle config --local frozen 'true' \
10 && bundle config --local without 'development test' \
11 && bundle install -j "$(getconf _NPROCESSORS_ONLN)"
12
13COPY . /src
14
15ENV RAILS_ENV production
16ENV RAILS_SERVE_STATIC_FILES yes
17ENV RAILS_LOG_TO_STDOUT yes
使用 ENV
指令將 RAILS_ENV
這個環境變數設定上去,同時我們會將 Assets(素材)也使用 Rails 來提供,因此還要加上 RAILS_SERVE_STATIC_FILES
來確保 Rails 會回傳 CSS、JS、圖片等檔案。
另一方面,我們不希望 Rails 將 Log 存到 ./log
目錄下,希望直接輸出到 Docker 上面顯示,因此還要加入 RAILS_LOG_TO_STDOUT
來讓 Docker 幫我們管理 Log。
設置啟動命令
接下來我們要使用 CMD
指令告訴 Docker 預設該用哪個命令執行。
1FROM ruby:2.7.4
2
3RUN mkdir -p /src
4COPY Gemfile Gemfile.lock /src
5
6WORKDIR /src
7RUN gem install bundler:2.2.32 \
8 && bundle config --local deployment 'true' \
9 && bundle config --local frozen 'true' \
10 && bundle config --local without 'development test' \
11 && bundle install -j "$(getconf _NPROCESSORS_ONLN)"
12
13COPY . /src
14
15ENV RAILS_ENV production
16ENV RAILS_SERVE_STATIC_FILES yes
17ENV RAILS_LOG_TO_STDOUT yes
18
19CMD ["bundle", "exec", "rails", "server"]
因為我們想要的是將 Rails Server 開啟,因此給予 ["bundle", "exec", "rails", "server"]
的選項,這裡所下的選項建議用陣列的方式呈現避免 Docker 判斷錯誤。
編譯素材
同時,我們要在容器中提供靜態的 Assets 因此需要呼叫 rake assets:precompile
來編譯這些靜態檔案,所以需要修改 Dockerfile 加入 Node.js 跟 Yarn 來提供前端檔案編譯的必要套件。
1FROM ruby:2.7.4
2
3RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
4 && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
5 && curl -fsSL https://deb.nodesource.com/setup_17.x | bash - \
6 && apt-get install -y nodejs yarn \
7 && mkdir -p /src
8
9COPY Gemfile Gemfile.lock /src
10
11WORKDIR /src
12RUN gem install bundler:2.2.32 \
13 && bundle config --local deployment 'true' \
14 && bundle config --local frozen 'true' \
15 && bundle config --local without 'development test' \
16 && bundle install -j "$(getconf _NPROCESSORS_ONLN)"
17
18COPY . /src
19
20ENV RAILS_ENV production
21ENV RAILS_SERVE_STATIC_FILES yes
22ENV RAILS_LOG_TO_STDOUT yes
23
24CMD ["bundle", "exec", "rails", "server"]
在這邊我們使用的是 Node.js 的安裝腳本,如果直接使用 apt-get install node
的話可能會安裝到我們不希望使用的版本,改為官方腳本就能透過修改 setup_17.x
來調整成我們需要的版本。
同時,我們在原本 mkdir -p /src
的命令前面使用 \
和 &&
的技巧將命令組成一個來避免存到多餘的檔案,這是在撰寫 Dockerfile 常見的技巧之一,通常後續還會加入「清除多餘檔案」的處理,在這邊我們就不多做討論。
1FROM ruby:2.7.4
2
3RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
4 && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
5 && curl -fsSL https://deb.nodesource.com/setup_17.x | bash - \
6 && apt-get install -y nodejs yarn \
7 && mkdir -p /src
8
9COPY Gemfile Gemfile.lock /src
10
11WORKDIR /src
12RUN gem install bundler:2.2.32 \
13 && bundle config --local deployment 'true' \
14 && bundle config --local frozen 'true' \
15 && bundle config --local without 'development test' \
16 && bundle install -j "$(getconf _NPROCESSORS_ONLN)"
17
18COPY . /src
19
20RUN bundle exec rake assets:precompile
21
22ENV RAILS_ENV production
23ENV RAILS_SERVE_STATIC_FILES yes
24ENV RAILS_LOG_TO_STDOUT yes
25
26CMD ["bundle", "exec", "rails", "server"]
最後加入 bundle exec rake assets:precompile
命令預先編譯 Assets,這裡我們將這個指令放在 ENV
的前面,避免我們在未來修改設定而造成需要重新生成的狀況。
測試容器
完成之後我們先使用 docker build -t myapp .
命令將 Rails 專案建置成鏡像。
接下來先使用 Docker 啟動一個 PostgreSQL 資料庫供我們的容器使用。
1docker run --name rails-db -e POSTGRES_PASSWORD=mysecretpassword -d postgres
大約一兩分鐘後我們就可以得到一個叫做 rails-db
的容器並且運行 PostgreSQL 資料庫。
1docker run --name myapp --rm -it --link rails-db -p 3000:3000 -e DATABASE_URL=postgres://postgres:mysecretpassword@rails-db/postgres myapp
接下來用上面的命令用我們的鏡像啟動專案,我們使用了 --link
跟資料庫連結,以及 -p 3000:3000
讓裡面的 Rails Server 可以被我們的電腦透過 http://localhost:3000
進行連線。
如果已經有建立一些資料表,那麼我們就還需要透過下面的命令運行 bundle exec rake db:migrate
來更新資料庫。
1docker run --rm -it --link rails-db -e DATABASE_URL=postgres://postgres:mysecretpassword@rails-db/postgres myapp bundle exec rake db:migrate
到這個階段,我們打開 http://localhost:3000
就會看到我們的 Rails 專案正常運作。這是最基本的 Ruby on Rails 容器化方式,實際上並不推薦在正式環境使用,然而在這個階段我們能夠做出可以部署的容器就已經足夠,我們會先繼續將部署的流程完成再繼續討論如何優化。
容器化最佳實踐
這篇文章中提到的處理方式屬於最為容易開始使用的方式,然而仍有許多可以改進的地方。像是減少大小、限制使用權限、清理多餘的檔案等等。
如果想要深入了解,可以參考Ruby on Rails 容器化最佳指南(一)和Ruby on Rails 容器化最佳指南(二)這兩篇文章。我在文章中將過去幾年對 Rails 專案進行容器化、優化的處理跟細節都彙整起來,目前使用的容器化解決方案大多能夠做到以約 100MB ~ 200MB 大小為單位進行部署,除了能夠快速部署之外也很容易管理。
如果想在第一時間收到更新,歡迎訂閱弦而時習之在這系列文章更新時收到通知,如果有希望了解的知識,可以利用Rails 部署實踐回饋表單告訴我。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- Rails 部署實踐 - 補上 Rails 教學缺少的一塊
- Rails 部署實踐 - 以容器部署 Rails 的方案
- Rails 部署實踐 - 部署前置準備
- Rails 部署實踐 - 容器化 Rails 專案概述
- Rails 部署實踐 - 上傳容器鏡像
- Rails 部署實踐 - 伺服器搭建
- Rails 部署實踐 - 撰寫 Docker Compose
- Rails 部署實踐 - 使用 HTTPS 協定加密連線
- Rails 部署實踐 - 健康檢查
- Rails 部署實踐 - 滾動更新
- Rails 實踐部署 - 使用 Alpine 製作容器鏡像
- Rails 部署實踐 - 容器化的 Bundler 最佳設定
- Rails 部署實踐 - 多階段建置
- Rails 部署實踐 - 素材預先編譯
- Rails 部署實踐 - 容器進入點
- Rails 部署實踐 - 容器相關工具
- Rails 部署實踐 - 持續部署
- Rails 部署實踐 - 使用 GitLab CI 自動化建置
- Rails 部署實踐 - 使用 GitHub Actions 自動化建置
- Rails 部署實踐 - 使用 Watchtower 自動更新
- Rails 部署實踐 - Docker Swarm 與 Docker Compose
- Rails 部署實踐 - Docker Swarm 安裝與設定
- Rails 部署實踐 - 部署到 Docker Swarm
- Rails 部署實踐 - 整合 GitLab CI 自動部署
- Rails 部署實踐 - 使用 GitLab 的 Review Apps 機制
- Rails 部署實踐 - 部署不是終點