【Docker】コンテナにソースコードをバインドマウントしている時のビルド方法

## はじめに

### 前提
Dockerの基本的な操作等については触れません。
バインドマウントを利用して、アプリのソースコードをコンテナ内にマウントして開発している状況を想定しています。

#### バージョン情報
Docker Engine: 19.03.13

### 大まかな流れ
1. まず結論
2. バインドマウントとは
3. 順を追ってビルド方法を確認していく
4. おまけ
5. 感想

### 当記事のゴール
バインドマウントの特徴を理解し、アプリのソースごとビルドして、イメージ化出来るようになる。

---

## まず結論
開発時にソースコードをコンテナにバインドマウントするのはよくある手法だが、デプロイなどの際にソースコードを含めてイメージ化するには一工夫する必要がある。
その方法は拍子抜けするほどシンプルだが、別途ビルド用のDockerfileを用意し、明示的にソースを`COPY`すればいい。
(自分で調べていて、「ほんとにこんなシンプルな話なの・・・？」と思わず疑ってしまった。)

---

## バインドマウントとは
Dockerで使えるボリュームのマウントタイプは主に3つある。
それぞれの違いを完結に説明すると

- ボリューム
    - Dockerによって管理される領域に対してマウントする
    - 複数のコンテナが共有可能
- バインドマウント
    - ホスト上のパスに対してマウントさせる
    - ホストとコンテナ、相互に読み書きが出来てしまう
- tmpfsマウント
    - ホストのメモリ上に保存される
    - コンテナが起動している間のみ、コンテナが利用するもの
    - 一時的な状態や機密情報などを保存する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136820/42da7541-0653-19dd-747b-c837362f63e6.png)

また、ボリュームとバインドマウントには以下のような特徴もある
[公式のストレージ概要](https://matsuand.github.io/docs.docker.jp.onthefly/storage/#tips-for-using-bind-mounts-or-volumes)から引用
> バインドマウントとボリュームを使う際のヒント
> バインドマウントとボリュームのどちらかを用いる場合には、以下のことを忘れないでください。

> コンテナー内のディレクトリに 空のボリューム をマウントしようとしていて、そのディレクトリ内にファイルやディレクトリが存在する場合、そのファイルやディレクトリはボリューム内にコピーされます。 コンテナー起動時に指定したボリュームがまだ存在していなかった場合は、空のボリュームが生成されます。 コンテナーの求めに応じて事前にデータを提供しておく方法として用いられます。

> コンテナー内のディレクトリに バインドマウントか、空ではないボリューム をマウントしようとしていて、そのディレクトリ内にファイルやディレクトリが存在する場合、マウントによってそのファイルやディレクトリは隠れてしまいます。 それはたとえば、Linux マシン上の /mnt にファイルを保存した後に、/mnt に対して USB ドライブをマウントしたような場合と同じです。 /mnt に存在していた内容は USB ドライブの内容によって隠されてしまい、USB ドライブがアンマウントされるまで続きます。 隠されてしまったファイルは、削除されるわけでなく変更もされません。 しかしバインドマウントやボリュームがアンマウントされない限り、アクセスすることはできません。



--- 

## 順を追ってビルド方法を確認していく
### シチュエーション
シンプルにNginxコンテナを構築し、ソースコードをコンテナにマウントさせ開発を行っていると想定する。
下記にサンプルのディレクトリ構成とDockerfileを示す。

```text:ディレクトリ構成
project-directory/
  └html/
    └index.html
  └Dockerfile

```

```dockerfile:Dockerfile
FROM nginx
COPY ./html /usr/share/nginx/html
EXPOSE 80
```

```html:index.html
<!DOCTYPE html>
<head>
<title>ContainerA</title>
</head>
<body>
    <h1>ContainerA!</h1>
</body>
```

### 下記のコマンドでソースコードがバインドマウントされたNginxコンテナを作る
```docker

docker run --name containerA --mount type=bind,source=(pwd)/html,target=/usr/share/nginx/html -d -p 81:80 nginx
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136820/8fd66c36-a90e-53aa-a792-6293caacb7f6.png)


#### コマンド解説
[DockerHub公式リポジトリのnginxイメージ](https://hub.docker.com/_/nginx)をベースに、以下のオプションでコンテナを作成する

- `--name`でコンテナ名を指定
- `--mount`でボリュームをマウント
    - ボリュームには旧来のオプションとして`-v or --volume`もあるが、公式で`--mount`を推奨しているので従う
    - `type`はマウントタイプを指定(今回はバインドマウントなので`bind`)
    - `source`はマウント元のパスを指定(ホスト)
    - `target`はマウント先のパスを指定(コンテナ内)
- `-d`でデタッチモード(バックグラウンドで起動)
- `-p`でポートフォワードを設定
    - ホストの`localhost:81`に対して、コンテナ内の`loclahost:80`を紐付ける

###  http://localhost:81 にアクセスして、画面を表示してみる
下記のような画面が表示される
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136820/189f0004-568f-af0f-2108-25519d3286e0.png)


### 文言を追加して、コンテナ内に反映されることを確認する
```html:index.html
<!DOCTYPE html>
~省略~
<body>
    <h1>ContainerA!</h1>
    <p>Add message at Host.</p>
</body>
```
再度読み込むと、追加した文言が表示されている。
これで、ホストとコンテナが確実にバインドマウントされていることも確認できた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136820/da6229b9-73d9-ea68-b828-58632f619b56.png)



### Dockerfileをビルドし、ソースごとイメージ化する
```docker

docker build -t build_with_bind_data ./
```

※`Dockerfile`が存在するディレクトリで実行する
↓は実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136820/03ca0c73-ab33-c16f-6387-c03baf5b4e28.png)


### ビルドされたイメージを確認する
```docker
docker image ls
```
下記のように`build_with_bind_data`というイメージがビルドされている

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136820/83f30b8b-f84a-5b43-f588-c6a9474c4950.png)


### ビルドしたイメージを元にコンテナを作成し、ソースも含められているか確認する

```docker

docker run --name containerB -d -p 82:80 build_with_bind_data
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136820/afe7de2a-d46f-6098-0bd2-74d6f8eb696a.png)

http://localhost:82 にアクセスすると
ContaierAで表示させていたものと全く一緒なのが確認できた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136820/77d24862-4190-79c2-0cf6-fd254a0b7a04.png)

一応、containerB内部のソースも確認しておくと

```bash

docker exec containerB bash -c "cat /usr/share/nginx/html/index.html"
```
ビルド時に、`COPY`コマンドによって、指定のディレクトリ/ファイルがコピーされていることが分かる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/136820/e3989fcb-cff6-3ee6-1255-63743e0cee19.png)


## おまけ
### `COMMIT`コマンドでコンテナはイメージ化出来るが、ボリュームマウントのデータは含まれない
`COMMIT`コマンドでイメージを固めたくなるが、これはあくまでコンテナ内部での設定や変更をイメージ化するものであり、<font color='red'>__コンテナにボリュームマウント[^1]されたデータは、コミットで作成されたイメージには含まれない__</font>、という点に注意する。
なので、ビルド時に`COPY`コマンドでアプリのソースコードを配置する必要がある。

[^1]: マウントタイプが、ボリューム/バインドマウント/tmpfsマウントのいずれか。

---

## 感想
多分内容としては初歩的なものです。
今までのDockerを使ってきた経験として、ビルドしてイメージを管理する機会が無かったので、ソースを含めたイメージ化の具体的な手法がわからず、苦戦しました。
`COMMIT`コマンドの特徴に気づかず、「なんでコミットしてもソースが含まれないんだ！？」なんて風に驚いていました。
が、公式の解説等をじっくり読んで自分なりに理解を深められたので、忘れないように記事に残しました。