弦而時習之

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

要將 Ruby on Rails 透過容器的方式部署,我們就需要讓我們的專案可以被容器化。這篇文章我們會採用最簡單的方式進行容器化,扣除掉有使用特殊的 Ruby Gem 的情況下,大多能夠透過這種方式完成容器化。

在開始之前,我會建議先看過如何在幾分鐘內容器化 Rails 專案這篇文章,裡面提供了一個更容易的方式來容器化你的專案。

Dockerfile 撰寫

目前最主流的容器化方式就是透過撰寫 Dockerfile 來實現這件事情,同時大部分容器的打包(Package)工具也都能夠支援這個格式。

撰寫 Dockerfile 的基本原則也不難,我們的目標就是將「安裝環境」的所有操作寫到 Dockerfile 裡面,並且讓打包工具幫我們轉換成鏡像(Image)用於部署。

建立 Dockerfile

首先,我們先在 Rails 專案下新增一個叫做 Dockerfile 的檔案,並且將以下內容放到裡面。

1
FROM ruby:2.7.5

這裡我們先使用 FROM 語法指定基礎的鏡像,要使用哪個版本的 Ruby 具體依照你目前的 Rails 環境為前提設定,這邊預設以目前新專案比較主流的 Ruby 2.7 為例子。

安裝 RubyGems

接下來我們要將 Ruby Gem 安裝進去,在使用 Docker 時前面的指令變化會影響後面的指令造成重新執行,因此我們會先將 GemfileGemfile.lock 使用 COPY 指令複製到容器中,讓我們可以先使用 RUN 指令執行 bundle install 來安裝 Ruby Gem。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
FROM ruby:2.7.5

RUN mkdir -p /src
COPY Gemfile Gemfile.lock /src

WORKDIR /src
RUN gem install bundler:2.2.32 \
  && bundle config --local deployment 'true' \
  && bundle config --local frozen 'true' \
  && bundle config --local without 'development test' \
  && bundle install -j "$(getconf _NPROCESSORS_ONLN)"

在這個範例中我們使用了正式環境的設定,因此會透過 bundle config 告訴 Bundler 要使用「部署模式」並且「凍結」來避免在安裝或執行的時候有我們預期以外的 Gem 被安裝進去。

導入專案

安裝完 Ruby Gem 之後,我們還需要將專案的原始碼複製到容器中才能夠執行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
FROM ruby:2.7.4

RUN mkdir -p /src
COPY Gemfile Gemfile.lock /src

WORKDIR /src
RUN gem install bundler:2.2.32 \
  && bundle config --local deployment 'true' \
  && bundle config --local frozen 'true' \
  && bundle config --local without 'development test' \
  && bundle install -j "$(getconf _NPROCESSORS_ONLN)"

COPY . /src

這個階段我們基本上就能夠直接用 bundle exec rails server 將伺服器啟動起來,然而我們還需要確保 RAILS_ENVproduction 來避免載入不必要的 Gem,因此需要再加入一些額外的調整進行設定。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
FROM ruby:2.7.4

RUN mkdir -p /src
COPY Gemfile Gemfile.lock /src

WORKDIR /src
RUN gem install bundler:2.2.32 \
  && bundle config --local deployment 'true' \
  && bundle config --local frozen 'true' \
  && bundle config --local without 'development test' \
  && bundle install -j "$(getconf _NPROCESSORS_ONLN)"

COPY . /src

ENV RAILS_ENV production
ENV RAILS_SERVE_STATIC_FILES yes
ENV 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 預設該用哪個命令執行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
FROM ruby:2.7.4

RUN mkdir -p /src
COPY Gemfile Gemfile.lock /src

WORKDIR /src
RUN gem install bundler:2.2.32 \
  && bundle config --local deployment 'true' \
  && bundle config --local frozen 'true' \
  && bundle config --local without 'development test' \
  && bundle install -j "$(getconf _NPROCESSORS_ONLN)"

COPY . /src

ENV RAILS_ENV production
ENV RAILS_SERVE_STATIC_FILES yes
ENV RAILS_LOG_TO_STDOUT yes

CMD ["bundle", "exec", "rails", "server"]

因為我們想要的是將 Rails Server 開啟,因此給予 ["bundle", "exec", "rails", "server"] 的選項,這裡所下的選項建議用陣列的方式呈現避免 Docker 判斷錯誤。

編譯素材

同時,我們要在容器中提供靜態的 Assets 因此需要呼叫 rake assets:precompile 來編譯這些靜態檔案,所以需要修改 Dockerfile 加入 Node.js 跟 Yarn 來提供前端檔案編譯的必要套件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FROM ruby:2.7.4

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
  && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
  && curl -fsSL https://deb.nodesource.com/setup_17.x | bash - \
  && apt-get install -y nodejs yarn \
  && mkdir -p /src

COPY Gemfile Gemfile.lock /src

WORKDIR /src
RUN gem install bundler:2.2.32 \
  && bundle config --local deployment 'true' \
  && bundle config --local frozen 'true' \
  && bundle config --local without 'development test' \
  && bundle install -j "$(getconf _NPROCESSORS_ONLN)"

COPY . /src

ENV RAILS_ENV production
ENV RAILS_SERVE_STATIC_FILES yes
ENV RAILS_LOG_TO_STDOUT yes

CMD ["bundle", "exec", "rails", "server"]

在這邊我們使用的是 Node.js 的安裝腳本,如果直接使用 apt-get install node 的話可能會安裝到我們不希望使用的版本,改為官方腳本就能透過修改 setup_17.x 來調整成我們需要的版本。

同時,我們在原本 mkdir -p /src 的命令前面使用 \&& 的技巧將命令組成一個來避免存到多餘的檔案,這是在撰寫 Dockerfile 常見的技巧之一,通常後續還會加入「清除多餘檔案」的處理,在這邊我們就不多做討論。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
FROM ruby:2.7.4

RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
  && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
  && curl -fsSL https://deb.nodesource.com/setup_17.x | bash - \
  && apt-get install -y nodejs yarn \
  && mkdir -p /src

COPY Gemfile Gemfile.lock /src

WORKDIR /src
RUN gem install bundler:2.2.32 \
  && bundle config --local deployment 'true' \
  && bundle config --local frozen 'true' \
  && bundle config --local without 'development test' \
  && bundle install -j "$(getconf _NPROCESSORS_ONLN)"

COPY . /src

RUN bundle exec rake assets:precompile

ENV RAILS_ENV production
ENV RAILS_SERVE_STATIC_FILES yes
ENV RAILS_LOG_TO_STDOUT yes

CMD ["bundle", "exec", "rails", "server"]

最後加入 bundle exec rake assets:precompile 命令預先編譯 Assets,這裡我們將這個指令放在 ENV 的前面,避免我們在未來修改設定而造成需要重新生成的狀況。

測試容器

完成之後我們先使用 docker build -t myapp . 命令將 Rails 專案建置成鏡像。

接下來先使用 Docker 啟動一個 PostgreSQL 資料庫供我們的容器使用。

1
docker run --name rails-db -e POSTGRES_PASSWORD=mysecretpassword -d postgres

大約一兩分鐘後我們就可以得到一個叫做 rails-db 的容器並且運行 PostgreSQL 資料庫。

1
docker run --name myapp --rm -it --link rails-db -p 3000:3000 -e DATABASE_URL=postgres://postgres:[email protected]/postgres myapp

接下來用上面的命令用我們的鏡像啟動專案,我們使用了 --link 跟資料庫連結,以及 -p 3000:3000 讓裡面的 Rails Server 可以被我們的電腦透過 http://localhost:3000 進行連線。

如果已經有建立一些資料表,那麼我們就還需要透過下面的命令運行 bundle exec rake db:migrate 來更新資料庫。

1
docker run --rm -it --link rails-db -e DATABASE_URL=postgres://postgres:[email protected]/postgres myapp bundle exec rake db:migrate

到這個階段,我們打開 http://localhost:3000 就會看到我們的 Rails 專案正常運作。這是最基本的 Ruby on Rails 容器化方式,實際上並不推薦在正式環境使用,然而在這個階段我們能夠做出可以部署的容器就已經足夠,我們會先繼續將部署的流程完成再繼續討論如何優化。

容器化最佳實踐

這篇文章中提到的處理方式屬於最為容易開始使用的方式,然而仍有許多可以改進的地方。像是減少大小、限制使用權限、清理多餘的檔案等等。

如果想要深入了解,可以參考Ruby on Rails 容器化最佳指南(一)Ruby on Rails 容器化最佳指南(二)這兩篇文章。我在文章中將過去幾年對 Rails 專案進行容器化、優化的處理跟細節都彙整起來,目前使用的容器化解決方案大多能夠做到以約 100MB ~ 200MB 大小為單位進行部署,除了能夠快速部署之外也很容易管理。


如果想在第一時間收到更新,歡迎訂閱弦而時習之在這系列文章更新時收到通知,如果有希望了解的知識,可以利用Rails 部署實踐回饋表單告訴我。

電子報

留言