Dockerのボリュームのマウントのされ方についてちょっとまとめました

令和(3年3月30日)最新版です。

# はじめに
Dockerのボリュームの記事は他にもあり大変参考になりますが、情報が古く私が疑問に抱いているところが解消できない部分もあったため改めて調査し記事にしました。
なんかこういう記事を乱立させるのもよくない気がしますが…。

本記事は`docker run`における`-v vol:/vol`や`docker-compose.yml`内の`volumes:`による指定を対象に書いています。
`tmpfs`等の他のストレージ管理方法については言及しませんのでご了承ください。

# 環境
- Ubuntu
  - 20.04.2 LTS
- Docker
  - verison: 20.10.5, build 55c4c88
- Docker Compose
  - version: 1.28.5, build c4eb3a1
  - ファイルフォーマット: 3.9

# 参考記事

- [DockerのVolume機能について実験してみたことをまとめます \- Qiita](https://qiita.com/namutaka/items/f6a574f75f0997a1bb1d)
- [Dockerのvolume問答 \- Qiita](https://qiita.com/SaitoTsutomu/items/4d73aaf7e80713c42a88)
- [Docker、ボリューム\(Volume\)について真面目に調べた \- Qiita](https://qiita.com/gounx2/items/23b0dc8b8b95cc629f32)
- [Docker の Volume がよくわからないから調べた \- Qiita](https://qiita.com/aki_55p/items/63c47214cab7bcb027e0)


# 結論
## 使い分け

- ホスト側の「設定ファイル」を読み込ませるのなら**バインドマウント**
- コンテナ側の「コンテンツデータ」を保存するのなら**名前付きボリューム**か**匿名ボリューム**

※理由は詳細説明を御覧ください。
※上記は必ずしもそうではなく「そういう場合が多い」程度に受け取っていただけたらと思います。



## DockerfileのVOLUME文とvolumesオプションを併用したときの挙動

- Dockerfileの`VOLUME`文は**匿名ボリューム化**を意味する。
- `docker run`または`docker-compose.yml`内で指定した`volumes`オプションが優先される。
  - `volumes`オプションがあれば、Dockerfileに`VOLUME`文を記述しても無視される。
  - ただし、`VOLUME /a`で`/a`ディレクトリを匿名ボリューム化して、`docker run -v ./hoge:/b`としても`/a`の匿名ボリュームは生成される。あくまで、コンテナ内のパスが重複した場合に`volumes`オプションが優先されるだけで、`volumes`オプションが**全てを上書きするわけではない。**



## ホスト・コンテナ両方にファイルが既に有った場合の挙動
- 基本的に**ホストが真**となる。
- ホストとコンテナ間での"マージ"のような動作はない。

各マウント方法ごとによる違いは下記。

- バインドマウント
  - 必ず**ホストが真**となる。
- 名前付きボリューム
  - Composeでボリュームを自動生成した場合は**コンテナが真**となる。
  - `external`等で既にファイルの存在するボリュームをコンテナにマウントした場合は**ホストが真**となる。
  - `external`等で空のボリュームをコンテナにマウントした場合は**コンテナが真**となる。
- 匿名ボリューム
  - 必ず**コンテナが真**となる。
- Dockerfileの`VOLUME`
  - 匿名ボリュームと同じ動作。必ず**コンテナが真**となる。
  - ただし、`run`や`compose`で`volumes`オプション指定があった場合は無視される。

## DockerfileのCMD文のタイミング
- `volumes`でコンテナ内のディレクトリがホストの内容になった**あとに**、CMDが実行される。

例：
![img01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180204/118e4ac9-6bc9-8f82-607e-c16653d2e0b3.png)


`CMD`がコンテナ実行時に自動実行されると考えると、`CMD`コマンドが実行されたあとにホストディレクトリに上書きされてしまうように思うが、`CMD`は**ホストディレクトリをマウントしたのちに実行される。**

上図の例だとコンテナの`/a`ディレクトリの中身が

```
container.txt
  ↓
container.txt
cmd.txt <========= echo "cmd">/a/cmd.txt
  ↓
host.txt
```

ではなく

```
container.txt
  ↓
host.txt
  ↓
host.txt
cmd.txt <========= echo "cmd">/a/cmd.txt
```

と変わる。

イメージを配布する際にユーザによる`volumes`オプションによってコンテナ内のデータが消えないような設計にするにはCMDを使うと良いかもしれないです。
実際にWordPressのDockerfileはそういう作りになっていました。(後述:[[ページ内リンク]](#%E8%A3%9C%E8%B6%B3cmd%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6))




# 詳細説明
まずはボリュームとは何か、を少しだけ触れます。

# ストレージマウントタイプの種類

Dockerが管理できるマウントの種類は大きく4つあります。

- ボリューム
  - Dockerコマンドで管理することのできるマウント。**「名前付き」**とランダムな文字列の**「匿名」**がある。
- バインドマウント
  - Dockerコマンドで管理することのできないマウント。ディレクトリ構造はホスト環境に依存する。
- tmpfsマウント
  - 一時的なマウント
- 名前付きパイプ
  - 使ってないのでわかりません😢

(参考)[Manage data in Docker \| Docker Documentation](https://docs.docker.com/storage/)

必ずしも **「マウント = ボリューム」** ではないことに注意してください。
ボリューム管理はDockerにおけるデータ管理方法のひとつです。
ここらへんの用語はDockerのドキュメント内においても定まってないような気がしてなりません。


`volumes`オプションで指定できるのが **「ボリューム」** と **「バインドマウント」** になります。
本記事はこちらについて解説します。

![img02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180204/2fc343fd-51a6-e60d-5833-1002a63fbc90.png)




# `volumes`オプションの種類
## ボリューム (Volumes)
**「名前付きボリューム(Named volume)」** と
**「匿名ボリューム(Anonymous volume)」** の2種類があります。

管理の仕方としては一緒です。違いは名前が**任意につけたもの**か**ランダムな文字列**かの違いだけです。

`docker volume ls`などでボリューム一覧を出したときに表示されます。
`docker volume rm <VOLUME>`でDockerコマンドから削除も可能です。

**「Dockerによって管理される」** というのが最大の特徴です。
ホストのファイルシステム・ディレクトリ構造に依存しないので可搬性が高いです。
環境に依存しないためコンテナ技術の思想とマッチしたボリューム管理方法です。

使うケースによりますが、2021年3月時点ではこの方法が[Docker公式により推奨されています](https://docs.docker.com/storage/#good-use-cases-for-bind-mounts)。

>In general, you should use volumes where possible. Bind mounts are appropriate for the following types of use case:

訳：
>一般的には可能な限りボリュームを使うべきです。バインドマウントは以下のようなときに適切でしょう。(略)

## バインドマウント (Bind mounts)
古くからあるマウント方法のようです。
ホストマシン上のディレクトリをDockerコンテナ内のディレクトリにマウントすることが出来ます。

**Dockerによって管理されません。**
よって、`docker volume ls`などでボリューム一覧を出したときに表示されません。

ホストの環境に依存するのでコンテナ技術の「データ管理」としては好ましくない設計でしょう。

また、公式で注意もされてますが
>One side effect of using bind mounts, for better or for worse, is that you can change the host filesystem via processes running in a container, including creating, modifying, or deleting important system files or directories. This is a powerful ability which can have security implications, including impacting non-Docker processes on the host system.

訳：[こちらからお借りしました](https://matsuand.github.io/docs.docker.jp.onthefly/storage/)
>バインドマウントを利用する際の副作用として、これが良いことか悪いことかはわかりませんが、コンテナー 内に動作するプロセスを通じて ホスト のファイルシステムに変更がかけられるということです。 **たとえばシステムにとって重要なファイル、ディレクトリを生成、編集、削除ができてしまいます。** **セキュリティに影響を及ぼしかねない強力な能力**がここにあって、ホストシステム上の Docker 以外のプロセスへも影響します。

つまり、ホスト上の`/etc/***`などの設定ファイル等ともコンテナと同期が取れてしまいますが、コンテナ上で変更があった場合**ホスト側にもその行動が反映**されてしまいます。
便利な場面もあるかと思いますが、危険でもあります。

---

以上の管理方法が`volumes`で設定できるマウント方法になります。
`volumes`の設定だけで**「名前付き」****「匿名」****「バインドマウント」**の3つあります。これがボリューム管理をややこしくさせている要因となっているように感じます。



# 各種挙動説明
以下の説明はすべて次に示すDockerfileをもとにbuildしています。
また作業フォルダは`~/test`です。

```Dockerfile:Dockerfile
FROM ubuntu

RUN mkdir /a ; \
    echo "Container">/a/container.txt
```

# 名前付きボリューム
名前付きボリュームは`docker run`や`docker-compose up`でコンテナ立ち上げ時に自動生成させた場合と、既にあるボリュームを参照する場合で場合分けします。

## 自動生成させたとき

下記`docker-compose.yml`を`docker-compose up`します。

```yaml:docker-compose.yml
version: "3.9"

services:

    ubuntu:
      build: .
      volumes:
        - testvol:/a
      tty: true

volumes:
  testvol:
```

すると、test_testvolという名前のボリュームが`/var/lib/docker/volumes/test_testvol/_data`に作られます。

そして、コンテナ内のファイルがボリューム内に書き込まれます。(同期されます。)

見た目上の動きは

**コンテナ → ホスト** です。


![img20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180204/2252ee37-86db-8a9c-8a37-9ce32c03300b.png)


## 既にある「空」のボリュームを参照したとき

事前に`docker volume create testvol`でボリュームを作成しておきます。
下記`docker-compose.yml`を`docker-compose up`します。

```yaml:docker-compose.yml
version: "3.9"

services:

    ubuntu:
      build: .
      volumes:
        - testvol:/a
      tty: true

volumes:
  testvol:
    external: true
```


コンテナ内のファイルが作成しておいたボリューム内に書き込まれます。(同期されます。)

見た目上の動きは
**コンテナ → ホスト** です。

![img21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180204/e246e77c-1f0a-8531-ddb2-5e3d83b688c1.png)


## 既にある「ファイルが存在する」ボリュームを参照したとき

事前に`docker volume create testvol`でボリュームを作成しておきます。
＋
**ファイルを置いておきます。**

```
$ sudo touch /var/lib/docker/volumes/testvol/_data/host.txt
```


下記`docker-compose.yml`を`docker-compose up`します。

```yaml:docker-compose.yml
version: "3.9"

services:

    ubuntu:
      build: .
      volumes:
        - testvol:/a
      tty: true

volumes:
  testvol:
    external: true
```

すると、コンテナ内は`container.txt`がなくなり、`host.txt`だけになります。



見た目上の動きは
**ホスト → コンテナ** です。


![img22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180204/ba0a607c-72e4-bef3-1de3-9faccc1909f9.png)





# 匿名ボリューム
匿名ボリュームは簡単です。`volumes`でコンテナのパスだけを指定することによって、Docker管理下のvolumesディレクトリに匿名ボリュームが作成されます。
Dockerfileの`VOLUME`も同等の機能です。
必ずコンテナ内が真となり、コンテナ内のデータは消えません。

下記`docker-compose.yml`を`docker-compose up`します。

```yaml:docker-compose.yml
version: "3.9"

services:

    ubuntu:
      build: .
      volumes:
        - /a
      tty: true
```

すると、`/var/lib/docker/volumes`配下にランダムな文字列のディレクトリが生成され、そこにデータが配置されます。


見た目上の動きは
**コンテナ → ホスト** です。



![img23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180204/b3a92a69-1ef0-a8cc-144b-4fa18b77041e.png)




# バインドマウント
必ずホストが真となります。
また、作業フォルダ等に配置されたディレクトリをマウントできるため、設定ファイル等をマウントする際に有効かと思います。
SSL証明書等のホスト上の決められたディレクトリにあるものをコンテナ内にマウントするときにも使います。

下記`docker-compose.yml`を`docker-compose up`します。

```yaml:docker-compose.yml
version: "3.9"

services:

    ubuntu:
      build: .
      volumes:
        - ./testvol:/a
      tty: true
```

すると、`./testvol`が無いときはDockerによって作成されコンテナにマウントされます。
`./testvol`を事前にユーザーが作成していた場合も、`./testvol`の内容がコンテナに上書きされます。
いずれにせよコンテナ内のデータは破棄され、ホスト上のディレクトリが真となります。

![img24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1180204/55c2b103-b2d1-745d-afb2-4556870939d2.png)




各種ボリュームの挙動についての説明は以上となります。


# 補足：CMDについて
ついでに軽く調べましたのでWordPressのDockerfileを例に説明します。

現時点最新版(5.7)のWordPressのDockerfileには下2行にこう記述があります。
注：作業フォルダは上位のPHPDockerfileにて"/var/www/html"になっています

```Dockerfile:Dockerfile(wordpress5.7)
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["apache2-foreground"]
```

意味はコンテナ起動時に
`docker-entrypoint.sh apache2-foreground`を実行するという意味です。

docker-entrypoint.shを見てみましょう。


```shell:docker-entrypoint.sh
～省略～
if [ ! -e index.php ] && [ ! -e wp-includes/version.php ]; then

～省略～

		echo >&2 "WordPress not found in $PWD - copying now..."

～省略～

		tar "${sourceTarArgs[@]}" . | tar "${targetTarArgs[@]}"
		echo >&2 "Complete! WordPress has been successfully copied to $PWD"
	fi
```

必要なところだけ抜粋しています。

`if [ ! -e index.php ] && [ ! -e wp-includes/version.php ]; then`
とあるので、`index.php`と`wp-includes/version.php`が両方共存在しないとき、ダウンロードしているWordPress本体を`/var/www/html`に解凍させる仕組みです。

なので、`/var/www/html`にホストの空のディレクトリをバインドマウントさせても、中身が消えてないように見えます。
`/var/www/html`に展開された後にホスト側と同期をとるので、ホストにもWordPressのファイル群が見える仕組みです。

if文に入らないように、index.phpだけ残してコンテナにマウントさせてコンテナを起動させれば

```:ホスト上のディレクトリ
~/wordpress/html$ ll
total 16
drwxr-xr-x 2 www-data   www-data   4096 Mar 30 02:43 ./
drwxrwxr-x 4 dockeruser dockeruser 4096 Mar 30 02:28 ../
-rw-r--r-- 1 www-data   www-data    261 Mar 27 22:50 .htaccess
-rw-r--r-- 1 root       root        405 Mar 30 02:43 index.php
```


```
dockeruser@DESKTOP-II2RT72:~/wdpress/wordpress$ ll
total 24
drwxr-xr-x 2 www-data   www-data   4096 Mar 30 02:43 ./
drwxrwxr-x 4 dockeruser dockeruser 4096 Mar 30 02:28 ../
-rw-r--r-- 1 www-data   www-data    261 Mar 27 22:50 .htaccess
-rw-r--r-- 1 root       root        405 Mar 30 02:43 index.php
-rw-r--r-- 1 www-data   www-data   5325 Mar 30 02:43 wp-config.php
```


このように、展開されませんでした。
(wp-config.phpは`docker-entrypoint.sh`内の他の記述によりコピーされてます。)



# 補足：externalとname

`external: true`の記述は`name: `でも代用できます。

```yaml
version: "3.9"

services:

    ubuntu:
      build: .
      volumes:
        - testvol:/b
      tty: true
volumes:
  testvol:
    name: testvol
 ```

これと

 ```yaml
version: "3.9"

services:

    ubuntu:
      build: .
      volumes:
        - testvol:/b
      tty: true
volumes:
  testvol:
    external: true
 ```

これは同じ動きをします。


# 補足：Data volume containerについて

初出がどこかわかりませんが…もっと古いDockerのバージョンであった考え方でしょうか？

Data volume containerはコンテナをボリューム専用のコンテナにし、データサーバーのようにして使います。
そうすることによって、`volume_from <コンテナ名>`でボリュームを共有できるようにしていました。

今(Composeフォーマット3.9)では名前付きボリュームでコンテナ間の共有はできますし`volume_from`自体、バージョン3で廃止されています。

# 他にも
他にもNFSとかSambaとかボリュームの作成方法がありますが、私自身触ったことないので理解できておりません。
ボリュームについても読み取り専用や権限等の他の設定項目があります。まだ調査しきれてないので掲載できず申し訳ありません。

以上で終わります。