---
title: "Rails 部署實踐 - 素材預先編譯"
date: 2022-05-06T00:00:00+08:00
publishDate: 2022-05-06T00:00:00+08:00
lastmod: 2025-10-19T10:51:10+08:00
tags: ["Rails","教學","部署","實作","Rails 部署實踐","Dockerfile","Assets"]
series: "rails-deployment-in-practice"
toc: true
permalink: "https://blog.aotoki.me/posts/2022/05/06/rails-deployment-in-practice-assets-precompile-in-container/"
language: "zh-tw"
---


目前大多數的 Rails 專案都還是前端、後端一起開發的，因此我們還需要讓製作出來的容器鏡像能夠將網站運行所需的圖檔、JavaScript 等等製作出來加入到容器鏡像中。

目前我們還沒有搭配持續交付（Continuous Delivery）相關的設計，因此我們先以直接在容器鏡像製作階段製作素材（Assets）的方式。

<!--more-->

## 利用多階段建置實現{#use-multi-stage-build}

目前 Rails 專案大多是以 Node.js 為基礎來編譯這些素材，然而我們的 Ruby 基礎鏡像並不會內建 Node.js，因此可以利用多階段部署來解決這個問題。

```dockerfile
# ...
FROM node:16.14-alpine as node
FROM ruby:3-alpine as assets

COPY --from=node /usr/local/bin/node /usr/local/bin/node
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

RUN mkdir -p /src

COPY . /src
WORKDIR /src

ENV RAILS_ENV production

RUN bundle exec rake assets:precompile

FROM ruby:3-alpine

# ...

COPY --from=assets /${APP_ROOT}/public /${APP_ROOT}/public

# ...
```

我可以直接下載 Node.js 的鏡像不做任何事情，因為 Node.js 的主程式只需要 `/usr/local/bin/node` 這單一一個檔案就能夠運行，因此我們可以直接將他複製到我們的容器鏡像中，讓我們可以呼叫 `node` 命令。

接下來將 Ruby Gem 也複製到這個建置階段，這樣也就不需要額外的安裝 Ruby Gem 的處理，剩下的就是將環境變數設定為 `RAILS_ENV=production` 來以正式環境的模式進行預先編譯。

當整個程序完成後，我們就可以將 `/src/public` 目錄下的 `packs` 以及 `assets` 兩個目錄複製到最終的鏡像中，來讓我們的 Rails 專案可以使用。

## Rails Credentials
然而，在靜態素材預先編譯時，可能會遇到一些狀況。像是我們有一些密鑰會保存在 Rails 6 加入的 Credentials（憑證）機制中，在編譯過程中會需要使用。

此時我們就會需要提供 Master Key（主鑰匙）到建置的環境裡面，因此可以透過 `ARG` 命令來允許傳入額外的設定。

```dockerfile
FROM ruby:3-alpine as assets

ARG RAILS_MASTER_KEY

# ...
ENV RAILS_ENV production
ENV RAILS_MASTER_KEY $RAILS_MASTER_KEY

RUN bundle exec rake assets:precompile
```

因為 Rails 是基於環境變數偵測的，因此在設定 `ARG` 命令後我們還需要利用 `ENV` 命令轉換為環境變數使用。

透過多階段建置，我們還額外獲得了 `RAILS_MASTER_KEY` 這類敏感資訊只會在建置階段中被保存，而不會被正式部署的鏡像紀錄，也提高了安全性。然而在建置的快取中仍會因為我們的 `ENV` 命令而被暫存到，因此在使用上還是需要多加注意。

> 如果你遇到了需要 Redis 或者資料庫的狀況，我會推薦你重新審視為什麼編譯靜態素材會需要連線 Redis 和資料庫，在大多數狀況下都是不太合理的情況。這有可能是專案的連線方式有問題，或者對靜態素材的定義實際上不太「靜態」

## 提供靜態素材{#serve-static-assets}

雖然我們已經具備了 `public/packs` 以及 `public/assets` 這兩個目錄，然而在 Rails 中正式環境（Production Mode，生產力模式）並不會處理這類靜態素材，因為這類檔案更適合由 NGINX 這類伺服器處理，吞吐量（Throughput）也會是讓 Rails 來處理高上數倍。

在這樣的情況，我們更適合搭配持續交付的工具來處理，如果是測試環境或者小型專案，我們可以透過設定來告訴 Rails 直接處理，而不是透過第三方的靜態檔案伺服器。

```dockerfile
FROM ruby:3-alpine as assets

# ...

FROM ruby:3-alpine

# ...

ENV RAILS_ENV=production
ENV RAILS_SERVE_STATIC_FILES=yes

# ...
```

自從 Ruby on Rails 5 開始，就陸陸續續加入許多 `RAILS_` 類型的環境變數，讓我們可以很輕鬆的針對這類常見的環境差異來進行設定。

在使用 `config/environments/production.rb` 這個檔案的狀況下，可以利用 `RAILS_SERVE_STATIC_FILES=yes` 的設定讓 Rails 來處理靜態素材，這樣我們建置出來的容器鏡像就能處理 JavaScript 這類檔案。

---

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

