Docker ComposeでRailsアプリ(Rails6.0.6/Ruby3.0.2 + PostgreSQL環境)をdocker化する


# はじめに
今回は既存のrailsアプリケーション環境をdocker化したので、そちらについてまとめていきます。dockerイメージの軽量化のためマルチステージビルドを採用しています。

# マルチステージビルドについて
**マルチステージビルド**とは、複数のイメージを用いたビルド方法のことです。何やら難しそうに聞こえますが、内容としてはシンプルです。
+ `FROM` を複数用意してそれぞれに名前を付ける（これが「**ステージ**」になります）
+ 各ステージは独立した一時的なイメージとして扱い、最終ステージだけが最終的な出力イメージとして保存される
+ 後のステージから前のステージを `--from` を使って参照でき、前のステージからファイルをコピーできる

マルチステージビルドを用いることで、**一つのDockerfileで複数のイメージをビルド**することができます。要は**Dockerイメージのビルドを効率的で、最適化されたものにするためのテクニック**です。

マルチステージビルドを使用する大きな目的は以下の2つになります
+ Dockerfileを読みやすく保守しやすくする
+ 余分な部分は削ぎ落とし必要なものだけをコピーして、最終的なイメージサイズを小さくする(これにより、ビルド時間の短縮にもつながる！)


参考資料
+ [マルチステージビルドの利用 - docker docs](https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/)
+ [マルチステージビルドの利用 - Docker-docs-ja 19.03](https://docs.docker.jp/engine/userguide/eng-image/multistage-build.html)
+ [Multi-stage build でNode.jsのインストールをちょっぴり効率化する](https://tech.actindi.net/2019/06/20/084247)


# 各種バージョン
+ ruby : 3.0.2
+ rails : 6.0.6.1
+ node : 16.20.1
+ yarn : 1.22.19
+ postgresql : 12.15


# rails環境のdocker化

## ファイル構成
今回は既存のrailsプロジェクトをdocker化します。railsプロジェクトのルート直下に以下の5つのファイルを準備します(Gemfile, Gemfile.lockがすでにある場合はそちらを使ってもらっても大丈夫です)
+ Dockerfile
+ docker-compose.yml
+ Gemfile
+ Gemfile.lock
+ .env

また、以下の3つのファイルを修正します
+ Gemfile
+ database.yml
+ .gitignore

## Dockerfile(追加)

DockerfileはDockerコンテナのイメージを構築するための手順を定義します。Dockerfileには、ベースとなるイメージの選択、新しいソフトウェアのインストール、環境変数の設定、ネットワークポートの開放、アプリケーションのコードのコピーなど、Dockerイメージの作成に必要な一連の手順が記述されます。

今回作成したDockerfileは以下のようになります。

```dockerfile:Dockerfile
FROM node:16-slim AS node

FROM ruby:3.0.2-slim

ENV YARN_VERSION 1.22.19
COPY --from=node /usr/local/bin/node /usr/local/bin/
COPY --from=node /opt/yarn-v$YARN_VERSION/ /opt/yarn/
RUN ln -s /usr/local/bin/node /usr/local/bin/nodejs \
    && ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
    && ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npx \
    && ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
    && ln -s /opt/yarn/bin/yarnpkg /usr/local/bin/yarnpkg \
    && apt-get update -qq \
    && apt-get install -y \
    build-essential \
    libpq-dev \
    shared-mime-info

WORKDIR /rails_app

COPY Gemfile Gemfile.lock package.json yarn.lock ./

RUN bundle install && yarn install

EXPOSE 3000

CMD ["rails", "s", "-b", "0.0.0.0"]

```
以下にコードの詳細をまとめました
(ちょっと長くなったので、必要な箇所だけ見ていただけたらと思います。。笑)

+ `FROM node:16-slim AS node`でNode.jsを含むDockerイメージをビルドするステージです
    + このステージは以下のRubyのイメージをビルドするステージにnodeとyarnをコピーするために使います
    + **node**という名前で参照できるように名前をつけています
+ `FROM ruby:3.0.2-slim`でRubyのバージョン3.0.2を含むDockerイメージを作成する基礎となるイメージを指定します
+ `-slim`がついたイメージは、フルイメージの下位互換バージョンです
    + インストール済みのパッケージは使用頻度の高いものに限定され、その分軽量化されています
    + 主に軽量のイメージが望ましい場合に使いますが、一部存在しないパッケージもあるので、正しく動作するかは十分なテストが必要です
    + [Dockerイメージ alpine,slim,stretch,buster,jessie等の違いと使い分け](https://www.ted027.com/post/docker-debian-difference/)
+ `ENV YARN_VERSION 1.22.19`でYarnのバージョンを環境変数として設定しています
+ `COPY --from=node /usr/local/bin/node /usr/local/bin/`と`COPY --from=node /opt/yarn-v$YARN_VERSION/ /opt/yarn/`で、nodeイメージからNode.jsとYarnをRubyイメージにコピーします
+ `RUN ln -s /usr/local/bin/node /usr/local/bin/nodejs ...`の部分でシンボリックリンクを作成し、必要な依存関係をインストールします
    + シンボリックリンクは、あるファイルへの参照のようなもので、それ自体は実際のファイルの内容を持っていないのですが、参照先のファイルへのリンクとして働きます
    + ここでは、それぞれのコマンドラインツール(node, npm, npx, yarn, yarnpkg)へのシンボリックリンクを`/usr/local/bin/`に作成しています
    + これにより、上記のツールがどのディレクトリからでも直接アクセスできるようになります
    + `usr/local/bin/`は通常、システムのパスに含まれています
    + `apt-get update -qq`でパッケージリストを更新し、`apt-get install -y ...`で必要なパッケージをインストールします
+ `WORKDIR /rails_app`で、以降の命令を実行する作業ディレクトリを指定します
+ `COPY Gemfile Gemfile.lock package.json yarn.lock ./`で、ホストマシンからDockerコンテナへ必要なファイルをコピーします
+ `RUN bundle install && yarn install`で、Rubyの依存関係(Gemfileに記載されたgem)とJavaScriptの依存関係(package.jsonに記載されたパッケージ)をそれぞれインストールします
+ `EXPOSE 3000`で、Dockerコンテナ内でアプリケーションが3000ポートでリッスンすることを示しています
    + `EXPOSE`命令自体はポートを開放するわけではありません
    + 実際のところ、EXPOSEはあくまでドキュメントとしての役割が大きく、人間やソフトフェアがそのDockerイメージを使う時にどのポートを開放すべきかを知るための情報を提供しています
+ `CMD ["rails", "s", "-b", "0.0.0.0"]`で、Dockerコンテナが起動した時に実行するデフォルトのコマンドを設定します
    + このコマンドでRailsサーバーを起動し、すべてのIPからの接続を許可します




## docker-compose.yml(追加)

docker-compose.ymlは、複数のDockerコンテナを定義し、一緒に起動、スケール、停止ができるようにするためのツールで、Docker Composeの設定ファイルです。

**Dockerfile**で`個々のコンテナの設定`に、**docker-compose.yml**は`複数のコンテナの相互作用やアプリケーション全体の設定`に使われると理解するとわかりやすいです。

今回作成したdocker-compose.ymlは以下のようになります。

```yaml:docker-compose.yml
version: '3'
services: 
  db:
    image: postgres:12
    volumes:
      - ./tmp/db:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

  web:
    build: 
      context: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/rails_app
    ports:
      - "3000:3000"
    depends_on:
      - "db"
```
以下にコードの詳細をまとめました

+ **dbサービス**
    + `image: postgres:12`で、DockerがDocker Hubからバージョン12のpostgresイメージをダウンロードし、これをもとにdbコンテナを作成します
    + 以下のコードでホストマシンの`./tmp/db`ディレクトリとコンテナ内の`/var/lib/postgresql/data`ディレクトリをマウントします。これにより、データベースのデータが永続化され、コンテナが再起動または破棄されてもデータが保持されます
        ```yaml
        volumes:
              - ./tmp/db:/var/lib/postgresql/data
        ```
    + 以下で、dbサービス(PostgreSQLのコンテナ)の環境変数を設定します
        ```yaml
        environment:
              POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
        ```
+ `webサービス`
    + `build:`でwebサービスのDockerイメージをビルドする際の設定をします
        + `context: .`でビルドコンテキストを指定します
    + `command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"`は、webサービスのコンテナが起動した時に実行されます
        + まず、`rm -f tmp/pids/server.pid`で不要なrailsサーバーのPIDファイルを削除し、新たにrailsサーバーを起動できるようにします
        + 次に、`bundle exec rails s -p 3000 -b '0.0.0.0'`でrailsサーバーをポート3000で起動します
    + 以下で、ホストのカレントディレクトリとコンテナ内の/rails_appディレクトリをマウントします。これにより、ホスト側でのソースコードの変更がコンテナに反映され、その逆も可能になります
        ```yaml
        volumes:
              - .:/rails_app
        ```
    + 以下で、ホストのポート3000とコンテナのポート3000をマッピングします。これにより、ホストから`http://localhost:3000`でrailsアプリにアクセスできます。
        ```yaml
        ports:
              - "3000:3000"
        ```
    + 以下は、webサービス(railsサーバー)がdbサービスに依存していることを示しています。これにより、Docker Composeはdbサービスを先に起動し、その後でwebサービスを起動します。
    + なお、`depends_on` はコンテナの起動順序を制御しますが、それが完全に準備（例えばデータベースが接続を受け付ける準備ができているなど）ができていることを保証するわけではないみたいです。。そのため、アプリケーション側でもデータベースなどのサービスへの接続が可能になるまで待機するロジックが必要な場合があります。
        ```yaml
        depends_on:
              - "db"
        ```
 

## .env(追加)
.envファイルは、アプリケーションの環境変数を定義するために使います。環境変数は一般的に、システムの設定情報や、データベースのパスワード、APIキー、その他の秘密情報など、環境に依存する値を格納する為に使います。ですので、今回はデータベースのパスワードはこちらに記載します。

```:.env
POSTGRES_PASSWORD=password
```



## .gitignore(修正)
.envファイルに記載している情報は秘密情報であり、これらを公開してしまうとセキュリティ上のリスクが生まれるので、`.envファイル`を管理対象から外します。

```gitignore:.gitignore
# ▼以下を追記する
/.env
```

## database.yml(修正)
デフォルトではSQLite用の設定になっているのですが、以下のようにPostgreSQLを使用する設定に変更します。passwordは.envに記載した環境変数を使用しています。

```yaml:database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  username: postgres
  password: <%= ENV.fetch("POSTGRES_PASSWORD") %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: rails_app_dev

test:
  <<: *default
  database: rails_app_test

production:
  <<: *default
  database: rails_app_prd

```

+ 以下の設定で、dbサービス名を指定することで、railsからPostgreSQLに接続できるようになっています。これは、`docker-compose.yml`で定義された各サービスは、Docker内部ネットワーク上で自動的に名前解決されるためです。つまり、railsはDocker内部ネットワーク上で`db`というホスト名でアクセス可能となります。
    ```yaml
    host: db
    ```

## Gemfile(修正)


```:Gemfile
# ▼以下の2行を削除
# Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.4'

# ▼以下の2行を追加
# Use pg as the database for Active Record
gem 'pg', '~> 1.2.3'  
```



# コンテナの起動
ファイルの作成と修正が完了したら以下の手順でコンテナを起動します。


1. データベースの作成

```sh
docker-compose run web rails db:create
```

```sh
docker-compose run web rails db:migrate
```

2. イメージをビルドしてコンテナを起動

```sh
docker-compose up
```

3. 以下のURLにアクセスして画面が表示されることを確認


http://localhost:3000/



# その他Tips
以下に動作確認時に使ったコマンドも記載しておきます。必要に応じて使ってみて下さい。

+ `docker-compose up`を実行して、すべてのコンテナが起動しているか確認したいときは`docker-compose ps`を実行
+ Dockerfileを更新したら、`docker-compose up --build` (buildしてrun)を実行
+ `node:16-slim`イメージで使えるyarnのバージョンを確認したい時
    + `docker run node:16-slim yarn --version`
+ `node:16-slim`イメージで使われているnodeのバージョンを確認したい時
    + `docker run node:16-slim node -v` 
+ postgresqlのバージョン確認したい時
    + `docker-compose exec -it db bash`
    + **dbコンテナ内**で以下を実行
    + `psql -U postgres -c 'SELECT version();'`







# 参考資料

以下はマルチステージビルドに関する公式ドキュメント・資料になります(再掲)
+ [マルチステージビルドの利用 - docker docs](https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/)
+ [マルチステージビルドの利用 - Docker-docs-ja 19.03](https://docs.docker.jp/engine/userguide/eng-image/multistage-build.html)
+ [Multi-stage build でNode.jsのインストールをちょっぴり効率化する](https://tech.actindi.net/2019/06/20/084247)

以下の動画は基本的なdocker、docker-composeを使ったrailsの環境構築で、とても分かりやすいです
+ [【rails環境構築】docker + rails + mysql で環境構築【FarStep】 - Youtube](https://www.youtube.com/watch?v=Fq1PH0Gwi8I)

docker-composeを使ってバックエンド、フロントエンド、データベースそれぞれをコンテナ化したSPA構成のアプリの環境構築をしたい方は、概要や全体像を理解するなら以下の動画がとても分かりやすいのでおすすめです！
+ [docker-compose による開発環境構築入門【やっすんのエンジニア大学】- Youtube](https://www.youtube.com/watch?v=CgWQmT0hQaM)

dockerの基礎を学ぶのに以下のUdemy(Docker関連で最高評価のコンテンツです)を活用しました。
+ [米国AI開発者がゼロから教えるDocker講座 -Udemy](https://www.udemy.com/course/aidocker/)