---
title: "Rails 部署實踐 - 多階段建置"
date: 2022-04-29T00:00:00+08:00
publishDate: 2022-04-29T00:00:00+08:00
lastmod: 2023-09-03T17:41:34+08:00
tags: ["Rails","教學","部署","實作","Rails 部署實踐","Dockerfile","多階段建置","Docker"]
series: "rails-deployment-in-practice"
toc: true
permalink: "https://blog.aotoki.me/posts/2022/04/29/rails-deployment-in-practice-multi-stage-build/"
language: "zh-tw"
---


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

<!--more-->

## 什麼是多階段建置{#what-is-multi-stage-build}

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

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

## 如何使用多階段建置{#multi-stage-usage}

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

```dockerfile
FROM ruby:3-alpine

RUN echo "HELLO WORLD" > /tmp/example.txt

FROM ruby:3-alpine

COPY --from=0 /tmp/example.txt /root
```

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


```dockerfile
FROM ruby:3-alpine AS source

RUN echo "HELLO WORLD" > /tmp/example.txt

FROM ruby:3-alpine

COPY --from=source /tmp/example.txt /root
```

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

## 改善 Rails 鏡像建置{#improve-rails-image-build}

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

```dockerfile
FROM ruby:3-alpine AS gem

RUN apk add --no-cache build-base

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

RUN gem install bundler:2.3.3 \
    && bundle config --local deployment 'true' \
    && bundle config --local frozen 'true' \
    && bundle config --local no-cache 'true' \
    && bundle config --local system 'true' \
    && bundle config --local without 'development test' \
    && bundle install -j "$(getconf _NPROCESSORS_ONLN)" \
    && find /src/vendor/bundle -type f -name '*.c' -delete \
    && find /src/vendor/bundle -type f -name '*.o' -delete \
    && find /usr/local/bundle -type f -name '*.c' -delete \
    && find /usr/local/bundle -type f -name '*.o' -delete \
    && rm -rf /usr/local/bundle/cache/*.gem

FROM ruby:3-alpine

COPY --from=gem /usr/local/bundle/config /usr/local/bundle/config
COPY --from=gem /usr/local/bundle /usr/local/bundle
COPY --from=gem /src/vendor/bundle /src/vendor/bundle
```

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

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

## 建置快取{#build-cache}

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

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

```bash
$ docker build \
  --target gem
  --cache-from rails:gem
  --tag rails:gem .

$ docker build \
  --cache-from rails:gem
  --cache-from rails:latest
  --tag rails:latest .
```

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

---

如果想在第一時間收到更新，歡迎[訂閱弦而時習之](https://mailchi.mp/aotoki/rails-deployment-in-practice)在這系列文章更新時收到通知，如果有希望了解的知識，可以利用[Rails 部署實踐回饋表單](https://us4.list-manage.com/survey?u=dd3d68032c0510041f1302539&id=f25e0dc43e&attribution=false)告訴我。

