RailsアプリにDockerを導入した①【webコンテナとdbコンテナ】

## やったこと
1. `Dockerfile`で、rubyイメージをベースにwebコンテナ（railsサーバー）のイメージを作成し、コンテナを起動してみた。
2. その後、`docker-compose.yml`を作成し、webコンテナとdbコンテナの２つのコンテナをまとめて起動できるようにした。


## 用語について
- __Docker Engine__: 各アプリケーションコンテナを載せている基盤部分のソフトウェア。Linuxカーネルを利用。Docker CLIを提供。

- __Docker CLI__: `docker container run`など、`docker`で始まるコマンドを提供。

- __Docker Desktop__: WindowsやMacでDockerを使うためのGUIアプリケーション。Docker EngineやLinuxカーネルが含まれているため（Docker Composeも）、Linux以外のOSでもDocker Engineを動かせる！

- __Docker Compose__: Docker CLIのコマンドをまとめて実行してくれる便利ツール。`docker compose`で始まるコマンドを提供。ymlファイルに実行させたいコマンドの手順書を記述する。

- __Docker Hub__: Dockerのイメージレジストリを提供するサービス。ここで公開されているイメージを`pull`したり、構築したイメージをここに`push`したりできる。

- __コンテナ__: ホストマシン上の隔離されたプロセス（LinuxのNamespaceという機能により他と分離されている）。イメージをもとに作られる。複数のコンテナは互いに独立し、影響を与えない。起動するたびに新しいコンテナが作られる。

- __イメージ__: コンテナの実行に必要なパッケージ。ファイルやメタ情報を集めたもの。複数のレイヤーからなる。Docker Hubで公開されている。

- __レイヤー__: ベース（例： Ubuntu）, インストールされているもの（例: PHP）, 環境変数, 設定ファイル, デフォルト命令などの情報。
    - デフォルト命令：コンテナ起動時に実行するコマンド。任意の命令を実行することも可能（コンテナ起動時に指定）。コンテナはメインプロセス（`PID=1`。デフォルト命令or起動時に指定された命令）が終了すると自動停止する。

- __Dockerfile__: 既存のイメージにさらにレイヤーを積み重ねるためのテキストファイル。既存のイメージをもとに、自分のアプリに合ったイメージになるようカスタマイズする。
    - `FROM`: ベースイメージを指定
    - `RUN`: 任意のコマンドを実行
    - `COPY`: ホストマシンのファイルをイメージに追加
    - `CMD`: デフォルト命令を指定
    - `ENV`: コンテナにインストールされているソフトウェアの環境変数を設定（=コンテナ内に環境変数を埋め込む）
    - `ARG`: Dockerfile内で使える変数を設定
    - `ENTRYPOINT`: コンテナの実行時にデフォルトで実行するコマンドや引数を設定。`CMD`にデフォルトの引数や推奨のパラメータを設定し、コンテナ起動時にオプション付与で引数・パラメータを任意に変更できる。
    - `EXPOSE`: コンテナの実行時、指定した ネットワーク・ポートをコンテナがリッスンするように、Docker へ通知。`docker run`で`-p`フラグを使い、ポートを指定することで実際にそのポートは公開される。

- __ボリュームのマウント__: コンテナ内のファイルの変更を、ホストマシンに共有する。ボリュームマウントとバインドマウントの２つの方法がある。
    - __ボリュームマウント__: Docker Engine上に領域（＝ボリューム）を確保し、コンテナ上にデータを保管する。データの保管場所は、コンテナごとに異なっている。Docker Hubの「Where to Store Data」の項目に記載がある。ホストマシンでの変更を反映させる必要がないものに向いている。（DBコンテナのデータなど）
    - __バインドマウント__: ホストマシンの任意のディレクトリをコンテナにマウントする。ソースコードの共有などに活用。ホストマシンでコード変更時に更新や同期の手間なく反映させることができる。絶対パスを指定することで、バインドマウントだと判断される。

## コマンド
- `container run [option] <image> [command]`: イメージからコンテナ起動　(`image pull`+`container create`+`container start`)
    - `--rm`オプション：コンテナ停止時に自動削除
    - `--interactive --tty`: コンテナを対話操作（コンテナの標準入力に接続＆疑似ターミナルを割り当て）
- `image build [option] <path>`: Dockerfileからイメージ作成
    - `--tag`オプション： イメージに名前をつけられる。イメージを参照・指定する際に便利。
    - `<path>`: `COPY`するファイルのパス
- `container exec [option] <container> command`: 起動中のコンテナに接続し、linuxコマンドを実行
    - `-it`オプション: コンテナを対話操作


## 手順
1. railsコンテナのイメージを作成
1. イメージのビルド `docker image build --tag rails:server .`
1. イメージがビルドできているか確認 `docker image ls`
1. railsコンテナとdbのコンテナを起動するようなdocker-compose.ymlの作成
1. railsコンテナが正常に起動することを確認 `docker compose up`→ブラウザで開けるか確認


## 実際に作成したDockerの設定ファイル

Dockerfile
```yml
FROM ruby:3.1.2
ARG ROOT="/karoyaka"
ENV TZ=Asia/Tokyo

WORKDIR ${ROOT}

# 必須ではないパッケージも一緒にインストールされないようにオプション付与
# MariaDB: mysqlから派生。高性能。
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
                  mariadb-client tzdata libvips

COPY Gemfile ${ROOT}
COPY Gemfile.lock ${ROOT}
RUN gem install bundler
RUN bundle install --jobs 4

EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]
```

docker-compose.yml
```yml
version: "3.9"
services:
  web:
    build: .
    command: bash -c "rm -f karoyaka/tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/karoyaka:cached
      - bundle:/usr/local/bundle
    ports:
      - '3001:3000'
    depends_on:
      - db
    stdin_open: true
    tty: true
    environment:
      MYSQL_PASSWORD: password

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '3307:3306'
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  bundle:
  mysql_data:
```

## それぞれのファイルで何をしているか
### Dockerfile
1. ベースイメージに`ruby:3.1.2`を指定
1. タイムゾーンの設定
1. 作業ディレクトリを命名して指定 
1. コンテナ内に必要なライブラリをインストール
    1. mariadb-client: MariaDBとは、MySQLから派生した高性能なRDBMS
    1. tzdata: タイムゾーンに関するデータ集
    1. libvips: 画像処理ソフトパッケージ（解析や変形）
1. GemfileとGemfile.lockを作業ディレクトリ内にコピー
1. bundlerをインストールし、gemたちもインストール（並列処理で高速化）
1. コンテナの実行時、指定したポート(3000)をコンテナがリッスンするように、Dockerへ通知（ポートの解放）
1. バインドするIPアドレスを指定してrailsサーバー起動
    - バインディングアドレス：外部からのリクエストを受け付けるために結び付けられているIPアドレス。
    - `0.0.0.0`=ワイルドカード。  
    - →全てのIPアドレスからのリクエストを受け付けられるようになり、ブラウザなどコンテナ外からもコンテナ内にアクセスが可能に。

### docker-compose.yml
1. webコンテナの定義
    1. ./Dockerfileに書かれたイメージからビルド
    1. pidファイルが存在していた場合、エラーになり新たにコンテナ起動ができないので削除する（`bash -c`: -cで渡される文字列をコマンドとして認識して実行する）。そして、すべてのIPアドレスをバインディングアドレスとして設定してrailsサーバー起動。（`command`は、Dockerfileに書いたデフォルト命令`CMD`を上書きする）
    1. ホストマシンのカレントディレクトリのソースコードをバインドマウント（`cached`:ホスト→コンテナへの反映を効率的に行うモード） ＆ bundleというボリュームに、インストールしたgemを保管。（rubyのコンテナでは、インストールされたgemは`usr/local/bundle`に入るように設定されている）
    1. ホストのポート番号は3001, コンテナのポート番号は3000に設定（ポートの定義によって、ホストマシンからコンテナへのアクセスが可能に）
    1. dbコンテナの起動後に起動するよう設定
    1. 標準入出力とエラー出力をコンテナに結びつけるよう設定。=`docker container run -i`
    1. 擬似端末(キーボードによる入力)をコンテナに結びつけるよう設定。=`docker container run -t`
1. dbコンテナの定義
    1. イメージを指定
    1. mysqlにログインするルートユーザのパスワードを定義
    1. ホストマシンのポート番号は3007, コンテナのポート番号は3006に設定
    1. mysql_dataというボリュームに、DBのデータを保管（永続化）
1. ボリュームの作成（bundleとmysql_data）

## これから
Dockerで、webコンテナとdbコンテナを起動させることができるようになった :tada: 
次はシステムテスト用のchromeコンテナ、そしてジョブ永続化のためにredisコンテナ、sidekiqコンテナも起動できるように設定していく！

## 参考
- [実践 Docker - ソフトウェアエンジニアの「Docker よくわからない」を終わりにする本](https://zenn.dev/suzuki_hoge/books/2022-03-docker-practice-8ae36c33424b59)
- [Dockerfile のベストプラクティス](https://docs.docker.jp/engine/articles/dockerfile_best-practice.html)
- [dockerfileのenvを使って環境変数を渡す【ARGとの違いも説明する】](https://blog.litely.net/ja/blog/docker_dockerfile_env/)
- [ENTRYPOINTは「必ず実行」、CMDは「（デフォルトの）引数」](https://pocketstudio.net/2020/01/31/cmd-and-entrypoint/)
- [シェルスクリプトの罠を避ける三つの tips](https://qiita.com/magicant/items/f3554274ee500bddaca8)
- [What does set -e and exec "$@" do for docker entrypoint scripts?](https://stackoverflow.com/questions/39082768/what-does-set-e-and-exec-do-for-docker-entrypoint-scripts)
- [](https://docs.docker.jp/engine/reference/builder.html)
- [rails s -b 0.0.0.0 のオプション-bの意味](https://qiita.com/Masato338/items/f162394fbc37fc490dfb)
- [Bind Addressの意味がようやく分かった](https://keens.github.io/blog/2016/02/24/bind_addressnoimigayouyakuwakatta/)
- [docker-compose.yml個人的Tips集](https://qiita.com/yuki_0920/items/dc3f32667d004979cc5a)
- [shellの-cオプションについてUbuntuのsh(dash)、bash、zshはそれぞれ違う挙動をする](https://qiita.com/ukinau/items/410f56b6d777ad1e4e90)
- [知識０から始めるRails on Docker](https://qiita.com/jintz/items/84f1782adbf7ea9bf56f)
- [既存のrailsプロジェクトをDockerで開発する手順](https://qiita.com/kkyouhei/items/653760627bf9d4bc9e71)
- [既存のRailsアプリにDockerを導入する手順](https://qiita.com/kenz-dev/items/b9e716204e0cd0cea447)
- [Ruby on Rails7.0.3 × MySQL8.0 のDocker環境構築](https://qiita.com/akky-tys/items/60d8ffce50b5600c5af4)