---
title: "Rails 實踐部署 - 使用 Alpine 製作容器鏡像"
date: 2022-04-15T00:00:00+08:00
publishDate: 2022-04-15T00:00:00+08:00
lastmod: 2023-09-03T17:41:34+08:00
tags: ["Rails","教學","部署","實作","Rails 部署實踐","Alpine","Dockerfile","容器"]
series: "rails-deployment-in-practice"
toc: true
permalink: "https://blog.aotoki.me/posts/2022/04/15/rails-deployment-in-practice-use-alpine-base-image/"
language: "zh-tw"
---


在[容器化 Rails 專案概述](https://blog.aotoki.me/posts/2022/02/25/rails-deployment-in-practice-rails-containerize-basic/)這篇文章，我們介紹了非常快速的 Rails 容器製作方式，然而這樣的方式除了容器鏡像會非常大之外，也包含了許多我們不需要在正式環境存在的套件，對安全性也會有索影響，因此接下來我們要透過 Alpine 來製作專為正式環境部署打造的容器鏡像。

<!--more-->

針對製作 Ruby 或者 Rails 專案的容器鏡像，我已經有 [Boxing](https://github.com/elct9620/boxing) 這個 Gem 可以協助，如果不想花時間自己製作的話，可以參考這個我平常用於開發 Rails 專案的工具。

## Apline 是什麼？{#what-is-alpine}

在 Linux 的生態系中存在許多發行版本（Distribution）像是 Ubuntu、 Fedora、RedHat 等等都是，在這之中針對容器所設計的輕量、安全的發行版本就是 Alpine。

我們平常在使用 Linux 的時候常常會利用像是 `apt`、`yum` 這些命令安裝套件，並且每個發行版本都會有不同的特性跟機制，這是因為不同發行版本會根據他們的需求、目的客製化套件管理系統（Package Management）等等預裝的軟體，其中 Alpine 裡面則是盡可能的減少不必要的預裝軟體，只保留能夠以容器運行最低限度的部分，同時也真對安全性進行加強。

## 精簡的 Linux 製作{#craft-minimal-linux}

要製作一個精簡的 Linux，我們實際上可以使用像是 [busybox](https://hub.docker.com/_/busybox) 或者 [scratch](https://hub.docker.com/_/scratch) 來做為基礎，因為這類基礎鏡像非常小，以 `scratch` 來說，因為是 Docker 預留的鏡像所以只要有安裝 Docker 就會自然具備這個鏡像，建置出來的大小取決於額外加入的檔案大小。

然而，大多數的主流程式語言幾乎無法獨立執行，因此還會需要額外一些 C 語言的函式庫等等，因此能夠直接使用 `busybox`、`scratch` 這類基礎鏡像的語言通常會是 C 語言、Golang 這類能夠靜態連結所需函示庫的類型。

在這樣的狀況下，我們想要製作一個最精簡的容器反而會苦於彙整整個相依的套件庫而花費大量的時間，在這樣的前提之下我們使用 Alpine 做為基礎就能大大的減少耗費的時間。

## 用 Ruby 鏡像小試身手{#practice-with-ruby-image}

在 Alpine 中有許多跟過去使用 Ubuntu、Fedora 不太一樣的體驗，因此我們先以 Ruby 的 Alpine 版本作為練習，使用 Ruby 版本的鏡像進行一些簡單的練習。

首先，我們先建立一個專案目錄叫做 `tzinfo` 用來當作這次練習的設定，並且加入 `tzinfo.rb` 這個檔案。

```ruby
# frozen_string_literal: true

require 'tzinfo'

pp TZInfo::Timezone.all_identifiers
```

我們使用 `tzinfo` 這個 Ruby Gem 將所有時區列出來，這個 Gem 也被使用在 Ruby on Rails 中作為時區轉換的工具。

接下來，我們以正常的方式撰寫 `Dockerfile` 並且嘗試執行這個檔案。

```dockerfile
FROM ruby:3

RUN gem install tzinfo

COPY tzinfo.rb /

CMD ["ruby", "/tzinfo.rb"]
```

最後用 Docker Build 和 Docker Run 命令來測試我們的容器。

```bash
$ docker build -t tzinfo-rb .
$ docker run -t tzinfo-rb
```

就可以看到時區被印出來在畫面上，接下來我們改為使用 Alpine 來進行測試。

> `docker run` 的 `-t` 跟 `docker build` 是不同的意思，這邊指的是 `TTY`，表示將容器的內容顯示到終端機（Termnial）上的意思，如果沒有指定是看不到執行的內容的。

```dockerfile
FROM ruby:3-alpine

RUN gem install tzinfo

COPY tzinfo.rb /

CMD ["ruby", "/tzinfo.rb"]
```

目前大多數主流的容器都會提供 `alpine` 的鏡像版本，在 Ruby 中使用的是 `版本-alpine` 的標籤（Tag）只需要稍作修改即可，大多數情況正式部署我們都會盡可能將所有服務都選用 `alpine` 版本來減少鏡像的大小。

重新進行 Docker Build 和 Docker Run 之後會發現出現了錯誤訊息。

```bash
/usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/data_source.rb:159:in `rescue in create_default_data_source': No source of timezone data could be found. (TZInfo::DataSourceNotFound)
Please refer to https://tzinfo.github.io/datasourcenotfound for help resolving this error.
        from /usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/data_source.rb:156:in `create_default_data_source'
        from /usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/data_source.rb:55:in `block in get'
        from /usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/data_source.rb:54:in `synchronize'
        from /usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/data_source.rb:54:in `get'
        from /usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/timezone.rb:235:in `data_source'
        from /usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/timezone.rb:158:in `all_identifiers'
        from /tzinfo.rb:5:in `<main>'
/usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/data_sources/zoneinfo_data_source.rb:232:in `initialize': None of the paths included in TZInfo::DataSources::ZoneinfoDataSource.search_path are valid zoneinfo directories. (TZInfo::DataSources::ZoneinfoDirectoryNotFound)
        from /usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/data_source.rb:157:in `new'
        from /usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/data_source.rb:157:in `create_default_data_source'
        from /usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/data_source.rb:55:in `block in get'
        from /usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/data_source.rb:54:in `synchronize'
        from /usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/data_source.rb:54:in `get'
        from /usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/timezone.rb:235:in `data_source'
        from /usr/local/bundle/gems/tzinfo-2.0.4/lib/tzinfo/timezone.rb:158:in `all_identifiers'
        from /tzinfo.rb:5:in `<main>'
```

這是因為 Alpine 盡可能減少不必要的套件，因此我們可以選擇安裝 Ruby 的 `tzinfo-data` Gem 來解決，也可以將 Linux 的 `tzdata` 套件安裝回去，為了練習我們會使用後者的方法。

```dockerfile
FROM ruby:3-alpine

RUN apk add tzdata
RUN gem install tzinfo

COPY tzinfo.rb /

CMD ["ruby", "/tzinfo.rb"]
```

我們使用了 `apk` 命令來加入 `tzdata` 這個套件，如此一來就能夠將額外的套件安裝進去，然而我們還有幾個可以改進的空間。

* 避免暫存 `apk` 套件管理工具的快取（Cache）
* 合併指令（現階段非必要）

因此可以修改為下面的版本

```dockerfile
FROM ruby:3-alpine

RUN apk --no-cache add tzdata \
    && gem install tzinfo

COPY tzinfo.rb /

CMD ["ruby", "/tzinfo.rb"]
```

我們將快取關閉後，雖然會稍微慢一些，然而可以觀察到鏡像是有一定程度的縮小的。

```bash
[elct9620] tzinfo % docker images tzinfo-rb
REPOSITORY    TAG       IMAGE ID       CREATED          SIZE
tzinfo-rb   latest    282f1d2af213   13 minutes ago   75.7MB
[elct9620] tzinfo % docker images tzinfo-rb
REPOSITORY    TAG       IMAGE ID       CREATED         SIZE
tzinfo-rb   latest    5db2d8c7cc44   3 seconds ago   73.4MB
```

在製作鏡像時有效地將這些「快取」清除，就可以逐步地縮小鏡像盡可能地讓正式版本的鏡像只存在必要的檔案，也能避免在被攻擊時讓駭客可以獲取到額外的資訊作為參考。

至於合併指令的技巧，主要是因為 `RUN` （所有）指令對 Docker 來說類似於 Git 的一個 Commit 單位，也就是說我們使用兩次 `RUN` 表示要個別存檔一次。這樣的處理會讓「刪除檔案」的動作沒有產生實際效果，因為在前一個步驟已經被存檔過。

簡單來說，如果建置過程中有一些敏感資訊，使用這個技巧也能讓一些「只存在記憶體」的步驟被確實地消除，而不會洩漏在記錄之中，在這次的練習中我們並沒有這樣的需求，因此也不需要特別處理，這邊只是作為示範使用。

---

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

