---
title: "Rails 部署實踐 - 撰寫 Docker Compose"
date: 2022-03-18T00:00:00+08:00
publishDate: 2022-03-18T00: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/03/18/rails-deployment-in-practice-write-docker-compose/"
language: "zh-tw"
---


現在我們已經有建置好的專案鏡像並且可以被任意伺服器存取，同時也有了能夠運行容器的環境（以 Docker 為基礎）接下來只需要將我們的專案運行起來即可。

 <!--more-->

 ## 登入 Registry 伺服器{#login-registry-server}

 我們在 [Rails 部署實踐 - 上傳容器鏡像](https://blog.aotoki.me/posts/2022/03/04/rails-deployment-in-practice-upload-image/)有在本機登入過 Registry 伺服器用於部署，然而在我們的伺服器上並沒有登入過，因此還需要在我們根據[Rails 部署實踐 - 伺服器搭建](https://blog.aotoki.me/posts/2022/03/11/rails-deployment-in-practice-setup-server/)中安裝好的伺服器運行一次登入的命令，讓我們可以順利的下載到容器鏡像。

```bash
$ ssh root@[伺服器位置]
```

進入伺服器後執行 `docker login` 的命令，登入到你的 Registry 伺服器中。

```bash
$ docker login quay.io
```

然而，這樣的做法其實會有一些缺點，因為我們很可能給予了過高的權限（可以下載任意容器）如果被不當利用的話，有可能會影響到其他專案。

因此建議大家根據 Registry 伺服器的文件，製作 Deploy Token 來控制可以部署的範圍，像是 [GitLab Deploy Token](https://docs.gitlab.com/ee/user/project/deploy_tokens/) 就能夠以專案、群組來進行限制，會比直接使用自己帳號登入更安全，也不容易因為換部門、離職調整權限而影響使用。

> 如果是上傳到公開的 Registry 就不需要登入，然而像是 Docker Hub 會因為登入而稍微提高流量額度，可以根據情況判斷。

## Docker Compose 安裝{#install-docker-compose}

雖然目前 Docker Compose 已經是官方的工具之一，然而安裝完畢 Docker 後並不會內建 Docker Compose 可以使用，因此我們需要自行在伺服器上進行安裝。

我們可以利用下面的命令進行安裝。

```bash
$ pip3 install docker-compose
```

假設找不到 `pip3` 這個命令的話，我們可以先用 `python -V` 或者 `python3 -V` 來確認一下目前伺服器上內建的 Python 版本，以 Ubuntu 為例子，如果是 3.x 的話可以用下面的命令安裝。

```bash
$ apt-get install pip3
```

因為新版的 Docker Compose 已經不相容於 Python 2.x 的版本，因此伺服器還是 Python 2.x 的狀況可能需要更新 Linux 發行版本，比較新的 Linux 發行版本大多陸續更換為 Python 3。

## Docker Compose 撰寫{#write-docker-compose}

我們在[Rails 部署實踐 - 容器化 Rails 專案概述](https://blog.aotoki.me/posts/2022/02/25/rails-deployment-in-practice-rails-containerize-basic/)時已經體驗過使用 `docker` 命令來執行我們的 Rails 專案，然而也很明顯地會發現要將資料庫、專案跑起來的指令不容易記憶，而且也不好管理。而 Docker Compose 讓我們可以用 YAML 的方式輕鬆管理，實際上使用起來會容易很多。

### 基本結構{#basic-structure}

Docker Compose 基本上分為兩個層級，第一個層級用來指定要「定義」的類型，像是服務、網路、卷宗（Volume）這類設定，我們可以想像他是設定一台虛擬電腦所需的硬體設備。

```yaml
version: '3.8' # 清楚描述使用的版本，方便未來查詢文件

services:
  # 定義容器要執行的地方

volumes:
  # 定義容器的卷宗（長期保存的資料）

networks:
  # 定義容器的網路
```

### 網路設定{#netowrk-configuration}

在 Docker 預設的狀況下，我們是無法直接存取到運行中容器的任何服務，因為他會運行在 Docker 預設的虛擬網路環境下，這樣的好處是在沒有授權的狀況下我們不能任意連接到其他容器，可以讓這些容器有效的隔離起來，增加安全性。

在 Docker Compose 的設定中可以讓我們很輕鬆的建立網路設定，可以想像我們在一台伺服器中切分出了好幾個虛擬的區域網路的感覺。

```yaml
versin: '3.8'

# ...
networks:
  frontend:
  backend:
```

可以根據自己的規劃切分，一般使用可以針對 Docker Compose 為單位統一建立一個網路群組即可。

> 在沒有特別設定的狀況下，Docker Compose 會自動建立來將所有服務彙整成一個網路，來跟其他使用 Docker Compose 啟動的服務作為區隔。

### 卷宗設定{#volume-configuration}

容器技術之所以被廣泛使用，是因為相比虛擬機器（Virtual Machine）能夠更快速的運行服務，即使隔離性（Isolation）沒有虛擬機器那麼好，依舊是非常實用的。即使如此，容器的運作基本上還是需要跟宿主（Host）有一定程度的隔離，因此我們也需要定義一個虛擬的硬碟給容器使用。

```yaml
version: '3.8'

volumes:
  postgres_data:
```

跟網路設定類似，我們只需要定義卷宗的名稱基本上就足以讓我們在部署階段使用。

### 服務設定{#service-configuration}

當我們將網路、卷宗設定完畢後，就可以來根據我們的需求定義服務，這裡我們會將 Rails 加上 PostgreSQL 資料庫一起設定起來。

```yaml
version: '3.8'

services:
  postgres:
    image: postgres:13.1
    restart: unless-stopped
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB
      - POSTGRES_USER
      - POSTGRES_PASSWORD
    networks:
      - backend
  rails:
    image: "registry.example.com/myapp:${VERSION:-latest}"
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB
      - RAILS_MASTER_KEY
    ports:
      - "80:3000"
    networks:
      - frontend
      - backend
    depends_on: # 在以下服務之後啟動
      - postgres
```

上面這段範例展示了幾個不同的設定，除了基本規則定義服務的名稱之外，讓我們來依序分析每一個設定的用意。

首先是 `image` 應該很容易理解，就是所使用的容器鏡像。在 Rails 的設定中使用了 `${VERSION:-latest}` 的寫法是利用 Docker Compose 可以讀取環境變數的特性，讓我們可以在不修改 Docker Compose 的狀況下指定版本，像是使用下面的指令更新。

```bash
$ export VERSION=v1.0.1
$ docker-compose up -d
```

接下來是非常重要的 `restart` 設定，預設狀況下當發生錯誤時會將容器直接停止，然而這個做法會讓服務中斷，尤其是我們還沒有 SRE（Site Reliability Engineering，網站可靠性工程）的角色在的時候，會延後恢復的時間，然而使用了 `unless-stopped` 的設定，就會自行嘗試重新啟動除非我們手動關閉，這樣如果只是遇到記憶體不足被關閉的狀況，還能夠自行恢復。

至於 `environment` 設定的寫法會有兩種，一種有指定數值另一種沒有。這個設定會將我們想傳遞給容器的數值當作環境變數轉進去，然而我們有一些密鑰類型的資訊不希望直接寫在 `docker-compose.yml` 這個檔案中（通常會被 Commit  到專案）就可以利用這個特性，讓 Docker Compose 從環境變數中抓取。

聽起來管理可能有點不方便，然而 Docker Compose 是支援 [Dotenv](https://github.com/bkeepers/dotenv) 這個將 `.env` 檔案載入為環境變數的，因此我們可以在部署的伺服器上建立一個 `.env` 檔案來管理這些設定。

```bash
# .env

# Rails
RAILS_MASTER_KEY=top-secret

# PostgreSQL
POSTGRES_DB=myapp
POSTGRES_USER=app_user
POSTGRES_PASSWORD=app^password
```

> 在 Docker 內部會有 DNS 解析，因此在 DATABASE_URL 中我們可以用 postgres 當作資料庫的伺服器位置，而不需要指定 IP 位置，因為 Docker 在這個狀況下會幫我們設定好 postgres 對應的 IP 位置。

而 `volumes` 和 `networks` 應該就很容易猜到，跟前面段落設定的網路和卷宗對應，然而在卷宗的設定上需要指定「來源：目標」這樣的格式，告訴 Docker 這個卷宗會對應容器中的哪個目錄，而我們一般只會在有需要持久化保存資料的容器做這樣的設定。

最後的 `ports` 用來跟宿主機的網路進行橋接（Bridge）這也是網路設定的一種模式，因為我們無法直接存取到容器的網路，因此需要透過橋接這個特殊模式將宿主機跟容器串連起來，在這邊我們設定宿主機的 `80` 埠（Port）會轉介到容器的 `3000` 埠，如此一來我們直接開啟伺服器的 IP 時就能直接連上我們的 Rails 專案。

## 啟動服務{#boot-services}

將 Docker Compose 撰寫完畢後，整合起來會得到類似這樣的設定檔（`docker-compose.yml`）

```yaml
version: '3.8'

services:
  postgres:
    image: postgres:13.1
    restart: unless-stopped
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB
      - POSTGRES_USER
      - POSTGRES_PASSWORD
    networks:
      - backend
  rails:
    image: "registry.example.com/myapp:${VERSION:-latest}"
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB
      - RAILS_MASTER_KEY
    ports:
      - "80:3000"
    networks:
      - frontend
      - backend
    depends_on:
      - postgres

volumes:
  postgres_data:

networks:
  - frontend
  - backend
```

要將服務啟動，只需要使用 `up` 指令即可。

```bash
$ docker-compose up
```

然而只是這樣設定的話，當我們關閉 SSH 連線服務也會一起被終止，因此我們要加上 `-d`（Daemon，守護行程化）的選項，讓我們的服務在背景中執行。

```bash
$ docker-compose up -d
```

Docker Compose 會將目前所處的資料夾作為專案名稱（也可以視為 Namespace）將服務建構出來執行。

> `docker-compose.yml` 並不需要跟原始碼共存，因此可以另外管理來避免無關人員直接存取到專案原始碼。

除了將服務運行起來之外，因為我們的 Rails 專案還需要進行資料庫遷移（Migration）更新到最新的資料庫版本，因此可以利用 `docker-compose run` 建立一個暫時的 Ruby on Rails 容器來呼叫這個命令。

```bash
$ docker-compose run --rm rails bundle exec rake db:migrate
```

我們加入了 `--rm`（Remove）選項，當我們以 Rails 容器為基礎執行完畢 `rake db:migrate` 任務後，自動清除這個多餘的容器來保持環境的乾淨，否則會一直有一個執行完畢後的容器殘存在我們的伺服器中。

到此為止，我們就已經順利的將原始碼變成一個可以在網路上存取的服務，然而還有 HTTPS 的設定等著我們去完善。

---

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


