webアプリをdocker化してみよう

## はじめに
ホストで作成したrailsアプリケーションを`Docker`、`Dockerfile`、`docker-compose`を利用してDocker化するまでを解説します。

## Dockerについて
仮想化ツールの一つで、昨今で最も聞く名前ではないでしょうか。
なぜここまで`Docker`と聞くように（デファクトスタンダードに)なったかというと、自分が使ってみての所感だとやはり、
 * 軽い
 * 早い
 * 環境を人に渡しやすい

といった辺りでしょうか。

実務はオフライン環境下でして…
どちらも使用するのですが、zip等に圧縮して渡すにしても全くもってサイズが違うので取られる時間が段違いです。

「これらの差はなぜ？」と言われると、カーネルを含む、含まないが一番と思います。

さて、本記事はあくまでDocker化の手順解説のため、さっくりとこの辺までとして、早速手順に入っていきます。

## Dockerfileを書こう
dockerを使おうという人は、既に耳にしているだろう単語ですが、実際のところ何が書かれているのでしょうか？
ざっくり一言でまとめると、「コンテナの中で何をするのか」が書かれていると言えると思います。
ひとまず、作成したDockerfileを例に見てみましょう。

```dockerfile:Dockerfile
# 初めに元となるDocker Imageを指定しています
FROM ruby:3.2.2

# コンテナ内でコマンドを実行して諸々インストールしています
RUN apt-get update && \
    apt-get install -y \
    build-essential \
    libpq-dev \
    nodejs \
    postgresql-client

# 今回はすでに手元にあるアプリをDocker化するので、コンテナ内に全てコピーします
COPY . /rails-docker

# 環境変数を設定しています
ENV APP_HOME /rails-docker

# 作業ディレクトリを指定しています
WORKDIR $APP_HOME

# 作業ディレクトリでコマンド実行されます
RUN bundle install
```

各行で何を行なっているか、コメントに簡単に記載しています。
毎行コマンドの前に決まって大文字の命令が付いていますね。
これらについて、いくつかピックアップします。

### Dockerfileによく登場する命令
| 命令 | 概要 |
|:-:|:-:|
| FROM  | 元になるイメージの指定（ローカルにない場合はリモートから取得される）  |
| RUN  | コマンドの実行と結果の確定 |
| CMD | コンテナ実行時のデフォルトコマンドまたは引数を設定 |
| ENTRYPOINT | コンテナ実行時のデフォルトコマンドまたはスクリプトを設定 |
| EXPOSE | コンテナ実行時にコンテナ内で使用するポートを指定 |
| ENV | 環境変数の設定 |
| ADD | ホストからリモートへファイルやディレクトリの追加（多機能） |
| COPY | ホストからリモートへファイルやディレクトリの追加（シンプル） |
| VOLUME | コンテナ内にボリュームを作成する |
| WORKDIR | コマンドを実行する場所（作業ディレクトリ）の指定 |

※その他のコマンドは取り扱いませんので、興味がある方は調べてみてください。

### 押さえておきたいポイント
* 基本的にDockerfileの先頭には`FROM`を記述する。
* RUN、COPY、ADDは新しいレイヤーを作成する。
    * 悪戯にレイヤーが作成されていくと、イメージの肥大化やビルド時間の増加などの不都合が起きる。
    * 最終的にはコマンドをまとめるなどして、これらの命令は少なく記述できると良い。
* Dockerfileの作成で試行錯誤の間はCacheを上手く使う。
    * ```dockerfile:１回目のビルド
      RUN hoge
      ```
    <br>
    
    * ```dockerfile:２回目のビルド
      RUN hoge # １回目のビルド済み
      RUN fuga
      ```
    <br>
    
    * レイヤーが増えることになりますが、このように書くことで２回目のビルド時は`RUN hoge`はcacheが使用されるので短い時間で済みます。
    <br>
    
    * ```dockerfile:最終的には一行にまとめる
      RUN hoge && fuga
      ```
* `ENV`での環境変数設定は、元々存在する環境変数と同名を使うと意図せず上書きされてしまう可能性があることを頭に入れておく。
* `WORKDIR`で最後に指定した場所がコンテナに入った時の初期位置になる。

この辺りを念頭に入れてDockerfileを書いていけると良いと思います。

## 作成したDockerfileからImageを作ろう
ここまでで、初めにあげたDockerfileの例や、各命令の意味合いを学んだことで、簡単なものは作ってみれるようになるかと思います。
作成したDockerfileはどのように使うのでしょうか？

先ほどの命令の概要に、さらっと`ビルド`という単語が出てきました。
使用するコマンドの解説に入ります。

### `docker build`コマンド
```docker:docker build
docker build {-t [image_name] { : [tag_name] }} {-f [Dockerfile path]} [Dockerfile Directory]
{}は省略可、[]は必須として見てください。
{}の中に[]があるのは、省略部を書くならこれは必須ですよということですね。
```

`-t`から始まるオプション部分を省略すると、イメージ名、タグ名は付けられず`None`となります。

さて、よく見るとDockerfileのあるディレクトリを書くだけで、どれをビルドしろと言った記述はないですね。
このコマンドは、指定したディレクトリ内で`Dockerfile`というファイル名を探してビルドします。

ちょっと管理しにくいですよね。
`-f [Dockerfile Path]`のオプションを付けることで、DockerfileのあるディレクトリのどのDockerfileを使うか指定できます。
これで、Dockerfileという名前の縛りからも解放されるわけですね。

まだ知っておかないと困ることがあります。

### Build Contextを知ろう
COPYやADDなどを行う際のファイルやディレクトリは`Build Context`に含まれている必要があります。（不足しているとエラーになります）
`Build Context`とは、先ほど解説した`docker build`コマンドを実行した際のカレントディレクトリになります。

基本的には、Dockerfileに記述するパスも管理しやすいので、`Build Context`内にDockerfileもあるといった形が多いかと思います。
下の画像のような形になります。
![スクリーンショット 2023-11-02 21.53.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3518748/39f302de-2ef0-4d07-745d-cdfb6b0b0114.png)

その他、
* `-f`オプションを利用することでコンテキスト外のDockerfileを参照することが可能
    * この時、指定したDockerfileに記述する相対パスは、そのDockerfileのディレクトリが基準
* 絶対パスで指定することで`Build Context`外のファイルへアクセスすることも可能

であることを知っておくと良いと思います。

## 作成したイメージからコンテナを作成してみよう

ここまででイメージが出来上がりました。
次に、作成したイメージからコンテナを作成しましょう。

`docker run`コマンドを使用します。
よく使うと感じるオプションを紹介します。

| オプション | 概要 |
|:-:|:-:|
| -d | コンテナをバックグラウンドで起動します |
| -it[^1] | ユーザーに分かりやすい対話モードで起動します |
| -p | ホスト、コンテナ間でポートマッピングします |
| -v | ホスト、コンテナ間でディレクトリ共有を設定します |
| --name | 起動するコンテナに任意の名前を設定します |
| --rm | コンテナから出る時、自動でコンテナを削除 |

※他にも多くのオプションがあります。実際に触る際に実現したいことに合わせて調べて見てください。

例えば、下記のように使用するイメージです。
```docker:docker run
docker run {-it} {--rm} {--name [コンテナ名]} {-v [host dir:container dir]} ¥ 
           {-p [host port:container port]} [image]
```

`docker run`コマンドでコンテナを作成したら、確認のため`docker ps`コマンドで現在のコンテナ一覧を見てみましょう。
使用するコマンドのオプションや、イメージによっては起動、実行されて即コンテナ終了している場合もあるので、`-a`オプションをつけてコンテナの状態に関わらず一覧に出しましょう。

Dockerfileの作成、それを利用してイメージを作り、イメージからコンテナを作成しました。
しかし、この`docker run`コマンドで感じたかもしれません。

「毎回オプションつけてコマンド打ってたら長いし間違えそうだし面倒じゃね？」

Dockerはそもそも、人に環境を展開していくのに便利なツールと言った側面があると思いますが、手順が複雑となってしまったら薄れてしまいますね。

[^1]:ほぼセットで使うので、この形で紹介しましたが、`-i`、`-t`の２つのオプションです。

## 便利に使うために`docker-compose.yml`を書こう

作成したイメージの使い方が決まって、コンテナ立ち上げ時のオプションによる様々な設定が決まったとします。
どこかに設定を書いておいて、短いコマンドで同じ状態を誰にでも簡単に作れるようにしたいですね。

その為には、`docker-compose.yml`ファイルを作成して、そこに書いていきます。
早速、今回私が作成したものを見ていきながら、解説していきます。

```yml:docker-compose.yml
# 当ファイルの形式はdocker-composeのversion3に準拠するという意味です
version: '3'

# コンテナのPostgresDBデータの永続性を確保する為
volumes:
  postgres-data:

# サービスを定義
services:
  # webという名前でコンテナサービスを定義
  web:
    # docker build . と同じ
    build: .
    # docker runに -v .:rails-docker を付けたのと同義
    volumes:
      - .:/rails-docker
    # コンテナ起動時にコンテナで実行するコマンドを指定
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    # docker runに --env POSTGRES_PASSWORD=postgres を付けたのと同義
    environment:
      - 'POSTGRES_PASSWORD=postgres'
    # docker runに -p 3000:3000 を付けたのと同義
    ports:
      - "3000:3000"
    # dbサービスが先に起動されるように記述しています
    depends_on:
      - db

  # dbという名前でコンテナサービスを定義
  db:
    # postgres version12のイメージを指定
    image: postgres:12
    # docker runに -v postgres-data:/var/lib/postgresql/data を付けたのと同義
    volumes:
      - 'postgres-data:/var/lib/postgresql/data'
    # docker runに --env POSTGRES_USER=postgres --env POSTGRES_PASSWORD=postgres を付けたのと同義
    environment:
      - 'POSTGRES_USER=postgres'
      - 'POSTGRES_PASSWORD=postgres'
```

コメントでどう言った意味合いがあるか記述していますが、触れるべき部分にはもう少し詳しく見ていきましょう。

:::note info
volumes
postgres-dataという名前のボリュームを作成しています。
その後、services > db > volumes でホストに作成したpostgres-dataとコンテナの/var/lib/postgres/dataを紐づけています。
/var/lib/postgres/dataは、postgresデータのデフォルト保存先です。
こうしておかないと、コンテナの削除等によりデータを損失したり、バックアップの取得が難しくなったりとよろしくない状況になります。
:::

:::note info
services > web > volumes
ホストのカレントディレクトリとコンテナのrails-dockerディレクトリを紐づけています。
これは、ホスト側でコードを編集するとコンテナ内のコードも編集することが可能となります。
修正や変更などでコンテナに入る必要がなくなるわけですね。
:::

:::note info
services > web > depends_on
dbを指定しています。
これはコメントにも書きましたが、先にpostgresを立ち上げるためです。
それが何故なのか、というのはwebコンテナからdbコンテナに接続する際、先に立ち上がっていてくれないとエラーになってしまうためです。
:::

肝要な部分はこの辺りかなと思います。

念のため触れておきますが、webについてはdockerfileからイメージを作成していますが、dbについてはimage: postgres:12と書いてあるだけですね。
これはホストにない場合、DockerHubからイメージをプルしてくれます。

## `docker-compose`コマンドでまとめてコンテナ作成しよう

先の項目で`docker-compose.yml`を作成したおかげで、簡単に立ち上げることが可能になりました。

`docker`コマンドではなく、`docker-compose`コマンドを使用します。

まず、ymlからコンテナを作成、起動するには
`docker-compose up`を使用します。

このコマンドがやってくれることとしては、イメージの作成とコンテナの起動です。
どちらもymlに記載した設定で行われます。
今回の場合、webコンテナ（rails）とdbコンテナ（postgres）の２つをまとめて処理してくれます。

`docker build .`
`docker run --name ... --env ... -v ... postgres:12`
`docker run --name ... --env ... -v ... -p ... web command...`
といった手順を踏む必要があったのが、１コマンドでかつ、非常に短くなりましたね！

`dockerfile`や`docker-compose.yml`を編集した場合には、`--build`オプションを付けると最新の状態で状態で再ビルドしてくれます。
`-d`オプションでバックグラウンドにもできます。

`docker-compose down`でまとめてコンテナの停止
`docker-compose ps`でコンテナの一覧を表示
といったコマンドも併せて覚えておくと良いですね。

また、同時に複数コンテナを立ち上げられるなら、DockerHubからのプルでなく、複数のdockerfileを用意しておいてイメージ、コンテナ作成を行いたいとも考えると思います。
ここで、`docker build`コマンドの`-f`オプションが活躍してくるわけですね。

## ２つのコンテナはどう繋がっている？

`dockerfile`にも`docker-compose.yml`にも特にネットワークに関する記述がなかったのに、繋がってるの？繋がってるとしたらなんで？
と疑問に思ったでしょうか。

`docker-compose up`を行った際に、dockerが自動でネットワークを作成し、webとdbを繋げてくれています。

`docker network ls`コマンドでネットワーク一覧が見れます。
![スクリーンショット 2023-11-04 22.09.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3518748/cb6f48d7-903a-0f39-7401-d86a8c32c69f.png)

一番下に、作成されたネットワークが追加されているのが見えますね。

`docker inspect`でネットワークIDか名前を指定すると詳しい内容が見れますので、
* ネットワーク
* webコンテナ
* dbコンテナ

それぞれのinspectの内容を見ると繋がっているのが見て取れるかと思います。

`docker-compose exec web bash`コマンドでwebコンテナに入り、
`psql -U postgres -h <dbコンテナのIP>　-p <dbコンテナのポート>`コマンドで
dbコンテナのpostgresにアクセスできることなんかも見てみると良いかもしれません。

## 補足

私がDocker化した環境では、手順としては完了ではありません。

しかし、ここはrailsのお話になるのであまり言及しませんが、`docker-compose up`の後、コマンドとしては
`docker-compose run web rails db:create`
`docker-compose run web rails db:migrate`
を使用してデータベースの作成、アプリーションのデータモデル更新を行う必要があります。

## さいごに

`dockerfile`、`docker-compose.yml`を用意しておくことで、これらをソースコードと一緒にgitなりでリポジトリに置き、READMEなどに簡単なコマンドで少ない手順を記載しておけば展開が容易になります。

docker、docker-composeへの理解に少しでも役立てば幸いです。

ここまで読んでいただき、ありがとうございました！