【Docker】Dokcerfileの機能整理

ビルドコンテキストやCMDあたりの理解があやふやな感じだったので基本をちゃんと理解しておこうと思います。

# Dockerfile主要コマンド
| コマンド      | 概要                                                        |
|:------------|:------------------------------------------------------------|
| **FROM**    | DockerHubのベースイメージを指定                                 |
| **COPY**    | 作成イメージにビルドコンテキストからコピーするファイルを指定          |
| **RUN**     | イメージビルド時に実行するコマンドを指定                           |
| **CMD**     | コンテナ起動時に実行するコマンドを指定                             |
| **ENTRYPOINT**  | コンテナ起動時に**必ず**実行するコマンドを指定                 |
| **USER**    | RUN、CMD、ENTRYPOINT時に実行するユーザーを指定                   |
| **WORKDIR**  | RUN、CMD、ENTRYPOINT、ADD、COPY時の作業ディレクトリを指定        |
| **ARG**     | イメージビルド時に指定する引数を指定                              |
| **ENV**     | コンテナ内の環境変数を指定                                       |
| **VOLUME**  | Docker volumeやホストマシンからマウント可能となるディレクトリを指定  |

##### 検証を省略したもの
| コマンド | 理由 |
|:--------|:----|
| **MAINTAINER**    | 非推奨のため。  |
| **LABEL**    | 作成イメージにメタデータを付与することができるが、有効活用できるイメージが湧かなかった。  |
| **ADD**     | **tar展開**や**リモートURLサポート**が特定機能として存在しているが、極力COPYやRUNの使用が推奨されている。 <br /> → キャッシュやイメージサイズのベストプラクティスの観点から <br /><br /> ADDの利用が推奨されているのは現状「**ローカルの tar ファイルを自動的に展開してイメージに書き込むとき**」のみ。 <br /> 参考：[docker docs](https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/dockerfile_best-practices/#add-or-copy) |
| **EXPOSE**    | 外部公開するポートを指定できるが、docker run -Pによるポート公開やコンテナ間連携をして初めて意味を成すので今回は検証見送り。 |
| **ONBUILD**  | ONBUILDを記述したイメージをベースイメージとして、他のイメージをビルドする際に実行するコマンドを指定できる。 <br />チーム開発でメンバーにイメージ配布するケースなどで活用できそうだが、とりあえず存在だけ知っておけばよさそう。 |
| **STOPSIGNAL**    | docker stopではデフォルトで`SIGTERM`の終了シグナルが送信されるが`SIGKILL`など別の終了シグナルを指定できるコマンド。存在だけ知っておけばよさそう。  |
| **HEALTHCHECK** | 文字通りコンテナのヘルスチェックができる。必要になったら改めて。 |
| **SHELL** | コマンド実行時のデフォルトシェルを上書きできる。例えばLinux上でのデフォルトシェルは`["/bin/sh", "-c"]`。シェルを`["/bin/bash", "-c"]`としたい場面に遭遇することもありそうだが、今回は見送り。 |


# 各コマンドの動作確認

検証用のサンプルを用意
```shell
% tree -a
.
├── .dockerignore
├── docker
│   └── Dockerfile
├── hello.txt
├── ignore.txt
├── startup.sh
└── volume
    └── volume.txt
```

* Dockerfile
```Dockerfile
###############################################
############## create base image ##############
###############################################
FROM ubuntu:20.04 AS base

# 「docker image build」実行時の引数addUserを受け取る
ARG addUser

# 1.useraddコマンドに「docker image build」実行時の引数addUserを指定
# 2.VOLUME検証用のディレクトリをコンテナ内の/に作成
# 3.my-volume2.txtを作成
RUN useradd -m $addUser && \
    mkdir /my-volume && \
    touch /my-volume/volume2.txt

# ビルドコンテキスト直下の.txtファイルをコンテナ内の/app/にCOPY
COPY --chown=$addUser:root *.txt /app/
# ビルドコンテキスト直下のstartup.shをコンテナ内の/にCOPY
COPY --chown=$addUser:root --chmod=755 startup.sh /

# ENTRYPOINT,CMD 命令時の作業ディレクトリを/appに指定
WORKDIR /app

# docker-volumeやホストマシンからマウント可能な/my-volumeを指定
VOLUME /my-volume

# 「docker container run」実行時のユーザ指定 -> デフォルトのrootユーザではなく$addUserでコンテナログイン
USER $addUser

# コンテナ実行時に確実に実行するデフォルトコマンドの指定
ENTRYPOINT [ "/startup.sh" ]

# ENTORYPOINTに渡す引数指定
CMD [ "use CMD" ]


###############################################
####### create develop image from base ########
###############################################
# マルチステージビルド:dev
FROM base AS dev

# 環境変数の複数指定
ENV MESSAGE="dev stage!" ADD_USER=$addUser


###############################################
###### create production image from base ######
###############################################
# マルチステージビルド:prod
FROM base AS prod

# 環境変数の複数指定
ENV MESSAGE="prod stage!" ADD_USER=$addUser

```

* .dockerignore
```.dockerignore
ignore.txt
```

* startup.sh
```shell
#!/bin/sh

echo "startup.sh argument:\n $@\n"
echo "loginUser:\n $(whoami)\n"
echo "current directory:\n $(pwd)\n"
echo "owner/authority:\n $(ls -l /startup.sh)\n $(ls -l)\n $(ls -l /my-volume)\n"
echo "env:\n $MESSAGE\n"

# コンテナ内確認用
/bin/bash
```

### docker image build

```shell
# -t: イメージ名をタグ付きで指定
# --target: マルチステージビルド
# --build-arg: DockerfileのARGで宣言する引数を指定
# -f: Dockerfileとビルドコンテキストの参照箇所を指定
% docker image build \
    -t my-image:0.0.1 \
    --target dev \
    --build-arg addUser="devUser" \
    -f ./docker/Dockerfile .
```
:::note
-fオプション
**-f指定なしの場合**：
ビルドコンテキスト内にDockerfileがあるものとして処理される
= ビルドコンテキストとDockerfileは同階層である必要がある

**-f指定ありの場合**：
ビルドコンテキストとDockerfileを別々に指定可能
:::

イメージビルド時の内部挙動は
`クライアントがdockerCLI実行 > REST APIでdockerデーモンへ「Dockerfile」「BuildContext」を送信`
となっている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537959/29b40ae7-aa66-6390-34e9-e6f4b5e14c36.png)
Dockerfile内のCOPYやADDで指定するコピー元のファイルは、**ビルドコンテキストにおけるルートディレクトリ起点のパス**で指定することになります。
コピー元のファイル指定について
・ホストマシンの絶対パスを指定するとエラー
・../で上位階層を指定するとエラー
になる理由はこの仕様のためなのだと理解しました。

また、dockerデーモンにビルド資材を移送しているので、資材サイズへの意識も重要になってくると思いました。（CI/CDが絡むと特に？）
・ビルドコンテキストは必要最低限の範囲で指定
・サイズの大きい不必要なディレクトリ/ファイルは`.dockerignore`で除外
ここら辺は最低限意識しながら使っていきたいと思います。

実践的なDockerfileの記述については以下記事がとても参考になりました。

https://qiita.com/Tsuyozo/items/c706a04848c3fbbaf055

#### build結果
```
[+] Building 3.8s (11/11) FINISHED                                                                                                   docker:desktop-linux
 => [internal] load build definition from Dockerfile                                                                                                 0.1s
 => => transferring dockerfile: 1.89kB                                                                                                               0.0s
 => [internal] load .dockerignore                                                                                                                    0.1s
 => => transferring context: 87B                                                                                                                     0.0s
 => [internal] load metadata for docker.io/library/ubuntu:20.04                                                                                      3.5s
 => [auth] library/ubuntu:pull token for registry-1.docker.io                                                                                        0.0s
 => [base 1/5] FROM docker.io/library/ubuntu:20.04@sha256:33a5cc25d22c45900796a1aca487ad7a7cb09f09ea00b779e3b2026b4fc2faba                           0.0s
 => => resolve docker.io/library/ubuntu:20.04@sha256:33a5cc25d22c45900796a1aca487ad7a7cb09f09ea00b779e3b2026b4fc2faba                                0.0s
 => [internal] load build context                                                                                                                    0.0s
 => => transferring context: 132B                                                                                                                    0.0s
 => CACHED [base 2/5] RUN useradd -m devUser &&     mkdir /my-volume &&     touch /my-volume/volume2.txt                                             0.0s
 => CACHED [base 3/5] COPY --chown=devUser:root *.txt /app/                                                                                          0.0s
 => CACHED [base 4/5] COPY --chown=devUser:root --chmod=755 startup.sh /                                                                             0.0s
 => CACHED [base 5/5] WORKDIR /app                                                                                                                   0.0s
 => exporting to image                                                                                                                               0.0s
 => => exporting layers                                                                                                                              0.0s
 => => writing image sha256:16380bcc17cd414d5bdba5e6108005870bf7021e2d3ed371c6e858764739df27                                                         0.0s
 => => naming to docker.io/library/my-image:0.0.1
```

`VOLUME` `USER` `ENTRYPOINT` `CMD` はビルド時は実行されていないことがわかります。

### docker container run

```shell
# -v: {ホスマシン内|docker-volume領域のパス}:{コンテナ内のパス}として各種volume領域にマウント
# -it: コンテナ内確認を可能にする
# --my-image:0.0.1: イメージ指定
% docker container run \
	-v $(pwd)/volume:/my-volume \
	-it \
    my-image:0.0.1
```

#### コンテナ実行結果
```shell
# ENTRYPOINTで指定したshellの引数にCMDに指定した文言が渡っていることを確認
startup.sh argument:
 use CMD

# USERコマンドが効いていることを確認
# ユーザ名がビルド時に指定した"devUser"となっており、ARGコマンドが効いていることを確認
loginUser:
 devUser

# WORKDIRコマンドが効いていることを確認
current directory:
 /app

# COPYコマンドが効いており、オプションで指定した--chown --chmodが適用されていることを確認
# ホストマシンの$(pwd)/volumeにマウントされていることを確認
owner/authority:
 -rwxr-xr-x 1 devUser root 234 Sep 18 08:53 /startup.sh
 total 0
 -rw-r--r-- 1 devUser root 0 Sep 12 06:22 hello.txt
 total 4
 -rw-r--r-- 1 devUser devUser 18 Sep 17 11:28 volume.txt

# マルチステージビルドができていることを確認
env:
 dev stage!

devUser@cd3d6257f57f:/app$
```

### その他検証時の気づき

#### ENTRYPOINTとCMDについて

- exec形式とshell形式が存在するが**推奨はexec形式**のため、基本はexec形式で考える方が良さそう
- CMDは単独使用とENTRYPOINTとの併用で動きが異なることを念頭におく
単独使用：`CMD ["実行ファイル","パラメータ1","パラメータ2"]`
ENTRYPOINTと併用：`CMD ["パラメータ1", "パラメータ2"]`（ ENTRYPOINT 命令に対するデフォルトのパラメータとして扱う）
- ENTRYPOINT/CMDで変数展開するためにはシェル形式(`CMD echo $HOME`) or シェル直接実行(`CMD [ "sh", "-c", "echo $HOME" ]`)にする必要がある
-> 変数展開するのはDockerではなく、あくまでシェルであることを認識しておく

できることが似ており違いがあまり理解できていませんでしたが、あくまで両者は別物として考えるのが良いかと思いました。

またimageのhistoryを見たときubuntuのデフォルトCMDとして`CMD ["/bin/bash"]`が指定されていますが、`CMD ["use CMD"]`を実行しない場合、`"/startup.sh"`には何も引数が渡されていませんでした。
なのでENTRYPOINTによってデフォルトのCMDは上書きされていると理解しました。
DockerfileでENTRYPOINTとCMDを併用するとCMDはENTRYPOINTの引数になるのだと解釈しています。
```shell
 % docker history 16380bcc17cd414d5bdba5e6108005870bf7021e2d3ed371c6e858764739df27
IMAGE          CREATED       CREATED BY                                       SIZE      COMMENT
16380bcc17cd   3 hours ago   ENV MESSAGE=dev stage! ADD_USER=devUser          0B        buildkit.dockerfile.v0
<missing>      3 hours ago   CMD ["use CMD"]                                  0B        buildkit.dockerfile.v0
<missing>      3 hours ago   ENTRYPOINT ["/startup.sh"]                       0B        buildkit.dockerfile.v0
<missing>      3 hours ago   USER devUser                                     0B        buildkit.dockerfile.v0
<missing>      3 hours ago   VOLUME [/my-volume]                              0B        buildkit.dockerfile.v0
<missing>      3 hours ago   WORKDIR /app                                     0B        buildkit.dockerfile.v0
<missing>      3 hours ago   COPY startup.sh / # buildkit                     212B      buildkit.dockerfile.v0
<missing>      3 hours ago   COPY *.txt /app/ # buildkit                      0B        buildkit.dockerfile.v0
<missing>      3 hours ago   RUN |1 addUser=devUser /bin/sh -c useradd -m…   334kB     buildkit.dockerfile.v0
<missing>      3 hours ago   ARG addUser                                      0B        buildkit.dockerfile.v0
<missing>      6 weeks ago   /bin/sh -c #(nop)  CMD ["/bin/bash"]             0B        
<missing>      6 weeks ago   /bin/sh -c #(nop) ADD file:233702cd816c07bc9…   72.8MB    
<missing>      6 weeks ago   /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B        
<missing>      6 weeks ago   /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B        
<missing>      6 weeks ago   /bin/sh -c #(nop)  ARG LAUNCHPAD_BUILD_ARCH      0B        
<missing>      6 weeks ago   /bin/sh -c #(nop)  ARG RELEASE                   0B
```

#### VOLUME

コンテナ内に`/my-volume/volume2.txt`をビルド時に作成していましたが、コンテナ実行後消えていました。
VOLUMEコマンドを実行しないと`volume2.txt`は存在していました。
つまりコンテナ実行時に`/my-volume`はホストマシンの`$(pwd)/volume`にマウントされ、マウントポイントの内容に上書きされるということが挙動からわかりました。


# まとめ
* 主要コマンドとビルドコンテキストあたりがしっかり理解できていればDockerfileの扱い自体は問題ないんじゃないかという印象
* Dockerfile自体が難しいというより、そのDockerfileで「どんなコンテナを立てるか」で構築難易度が決まるはず
* どちらかというとベースイメージに使用するサーバや機能追加するパッケージのセットアップなどの周辺知識が重要そう（RUNコマンドをどう組んでCMDで何を実行させるか...など）


# 参考
* https://www.udemy.com/share/108o7U3@uOePIU7f6wwkOUyGa6GTousi8qwjDpBjt87HBMb1xAEHpGRNg-2wcqKKtUwdlB7NTg==/
* https://docs.docker.jp/index.html
* https://matsuand.github.io/docs.docker.jp.onthefly/engine/reference/builder/
* https://qiita.com/uehaj/items/e6dd013e28593c26372d
* https://qiita.com/Tsuyozo/items/c706a04848c3fbbaf055