蒼時弦也
蒼時弦也
資深軟體工程師
發表於

Rails 部署實踐 - 容器化 Rails 專案概述

這篇文章是 Rails 部署實踐 系列的一部分。

要將 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 時前面的指令變化會影響後面的指令造成重新執行,因此我們會先將 GemfileGemfile.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_ENVproduction 來避免載入不必要的 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 部署實踐回饋表單告訴我。