---
title: "Rails 部署實踐 - 容器化 Rails 專案概述"
date: 2022-02-25T00:00:00+08:00
publishDate: 2022-02-25T00:00:00+08:00
lastmod: 2023-09-03T17:41:34+08:00
tags: ["Rails","教學","部署","實作","Rails 部署實踐","容器","Docker"]
series: "rails-deployment-in-practice"
toc: true
permalink: "https://blog.aotoki.me/posts/2022/02/25/rails-deployment-in-practice-rails-containerize-basic/"
language: "zh-tw"
---


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

<!--more-->

在開始之前，我會建議先看過[如何在幾分鐘內容器化 Rails 專案](https://blog.aotoki.me/posts/2021/12/19/containerize-ruby-on-rails-in-a-few-minutes/)這篇文章，裡面提供了一個更容易的方式來容器化你的專案。

## Dockerfile 撰寫{#write-dockerfile}

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

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

### 建立 Dockerfile{#create-dockerfile}

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

```dockerfile
FROM ruby:2.7.5
```

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

### 安裝 RubyGems{#install-ruby-gems}

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

```dockerfile
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 被安裝進去。

### 導入專案{#load-projects}

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

```dockerfile
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_ENV` 為 `production` 來避免載入不必要的 Gem，因此需要再加入一些額外的調整進行設定。

```dockerfile
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。

### 設置啟動命令{#setup-command}

接下來我們要使用 `CMD` 指令告訴 Docker 預設該用哪個命令執行。

```dockerfile
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-precompile}

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

```dockerfile
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 常見的技巧之一，通常後續還會加入「清除多餘檔案」的處理，在這邊我們就不多做討論。

```dockerfile
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` 的前面，避免我們在未來修改設定而造成需要重新生成的狀況。

### 測試容器{#test-container}

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

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

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

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

```bash
docker 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` 來更新資料庫。

```bash
docker 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 容器化方式，實際上並不推薦在正式環境使用，然而在這個階段我們能夠做出可以部署的容器就已經足夠，我們會先繼續將部署的流程完成再繼續討論如何優化。

## 容器化最佳實踐{#containerize-best-practice}

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

如果想要深入了解，可以參考[Ruby on Rails 容器化最佳指南（一）](https://blog.aotoki.me/posts/2021/10/09/the-best-practice-of-containerize-ruby-on-rails-part-1/)和[Ruby on Rails 容器化最佳指南（二）](https://blog.aotoki.me/posts/2022/01/28/the-best-practice-of-containerize-ruby-on-rails-part-2/)這兩篇文章。我在文章中將過去幾年對 Rails 專案進行容器化、優化的處理跟細節都彙整起來，目前使用的容器化解決方案大多能夠做到以約 100MB ~ 200MB 大小為單位進行部署，除了能夠快速部署之外也很容易管理。

---

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

