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

經過兩週的努力,我們已經可以製作出能夠運行 Ruby on Rails 的環境,然而這個狀態的環境依舊同時混合了「編譯」跟「運行」兩種狀態的套件,在過去我們需要透過 RUN 合併命令來清理,然而在新版的 Docker 提供了「多階段建置(Multi-Stage Build)」的選項,因此我們可以直接切割開來處理。

什麼是多階段建置

多階段建置的機制很有趣,原本我們在 Dockerfile 所撰寫的建置腳本只能產生一個容器鏡像,然而多階段建置允許我們產生多個鏡像。

透過這樣的方式,我們可以將安裝 Ruby Gem、素材預先編譯等等步驟分離開來,最後再利用 COPY 命令,將所需的檔案複製到最終生成的鏡像,這樣就可以避免許多建置階段產生的額外檔案被加入到部署用的鏡像之中。

如何使用多階段建置

要使用多階段建置非常容易,基本上只需要多次使用 FROM 即可。

1FROM ruby:3-alpine
2
3RUN echo "HELLO WORLD" > /tmp/example.txt
4
5FROM ruby:3-alpine
6
7COPY --from=0 /tmp/example.txt /root

我們最後建置的鏡像會是最後一個 FROM 所對應的,一般來說用 --from 選項可以指定要複製的來源,然而這樣並不容易管理,因此還會搭配 AS 來進行命名。

1FROM ruby:3-alpine AS source
2
3RUN echo "HELLO WORLD" > /tmp/example.txt
4
5FROM ruby:3-alpine
6
7COPY --from=source /tmp/example.txt /root

像這樣子偷過 AS 的標記,就會更容易管理不同階段的建置資訊,除此之外也能將不同的建制處理區分出來,更容易釐清每個步驟的意圖。

改善 Rails 鏡像建置

接下來就可以用多階段部署的方式,將我們在前幾週的處理,調整成多階段的版本,將多餘的檔案再次排除,近一步減少容器鏡像的大小。

 1FROM ruby:3-alpine AS gem
 2
 3RUN apk add --no-cache build-base
 4
 5RUN mkdir -p /src
 6COPY Gemfile Gemfile.lock /src
 7
 8RUN gem install bundler:2.3.3 \
 9    && bundle config --local deployment 'true' \
10    && bundle config --local frozen 'true' \
11    && bundle config --local no-cache 'true' \
12    && bundle config --local system 'true' \
13    && bundle config --local without 'development test' \
14    && bundle install -j "$(getconf _NPROCESSORS_ONLN)" \
15    && find /src/vendor/bundle -type f -name '*.c' -delete \
16    && find /src/vendor/bundle -type f -name '*.o' -delete \
17    && find /usr/local/bundle -type f -name '*.c' -delete \
18    && find /usr/local/bundle -type f -name '*.o' -delete \
19    && rm -rf /usr/local/bundle/cache/*.gem
20
21FROM ruby:3-alpine
22
23COPY --from=gem /usr/local/bundle/config /usr/local/bundle/config
24COPY --from=gem /usr/local/bundle /usr/local/bundle
25COPY --from=gem /src/vendor/bundle /src/vendor/bundle

透過多階段部署,我們在第一階段加入 build-base 套件,安裝了可以編譯 C Extension 的環境,讓我們可以順利的把運行 Ruby on Rails 所需的 Gem 安裝。

接下來我們再從這個階段產生的檔案中,單純複製 Ruby Gem 的部分,這樣就可以避開 build-base 套件產生的檔案,進而縮小鏡像大小。

建置快取

雖然多階段建置可以製作非常乾淨的容器鏡像,然而原本的快取機制會無法正常運作。這是因為每一個階段都是單獨的鏡像,因此我們在進行建置的時候需要使用 --cache-from 選項,告知每一個階段的鏡像為何。

除此之外,也需要用 --target 選項先將不同階段的鏡像建置好,並且用於快取,也因此我們的建置指令可能會變成這樣。

1$ docker build \
2  --target gem
3  --cache-from rails:gem
4  --tag rails:gem .
5
6$ docker build \
7  --cache-from rails:gem
8  --cache-from rails:latest
9  --tag rails:latest .

如果沒有先另外進行建置,每次要進行多階段建置的時候還是會需要執行前面幾個階段的建置,而不會使用快取,因此在使用上還是需要多加注意。


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