Docker Volume (特に volumeタイプ) のわかりづらいところを説明してみる

Dockerにはイメージやコンテナとは別にボリュームというデータを永続化する機構があります。

「ボリューム？ホストのファイルをコンテナ内で使えるようにするやつでしょう？」と思ったあなたは1/3だけ正しいです。
Dockerのボリュームには `volume`, `bind`, `tmpfs` の三種類があり、ホストのファイルシステムの一部をコンテナ内で使えるようにするのは `bind` タイプの機能です。

Dockerのボリュームという概念はイメージ、コンテナを普段使っている人でもあまり深く使うことがない機能だと思います。またdocker run時のボリューム関係のコマンドも個人的には直感的ではないと感じるものがあります。

この記事ではそんなDockerのボリュームについての小ネタをいくつかご紹介します。

# 諸注意
- この記事に出てくるコマンドは Docker Engine 18.09 (ce) および Docker Desktop Version 2.0.0.0-mac81 (29211) で行いました。
- ベースイメージはなんでもよかったのでBusyBoxを使いました。
- 誤りなどありましたらコメントかTwitterなどで教えていただけると助かります。

# Docker Volumeのいろいろ

## イメージ内のデータを持つボリュームの基本の作り方

Dockerfileでは[VOLUME命令](https://docs.docker.com/engine/reference/builder/#volume)でどのパスをボリュームとしてマウントできるようにするか定義できます。

```Dockerfile
FROM busybox
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol
```

このDockerfileからコンテナを作成するときにコンテナの `/myvol` を指定してボリュームを作ることができます。[^myvol]

[^myvol]: `/myvol` はDockerfileのVOLUMEで指定しているのと同じパスですが、同じパスである必要はないです。あとで同じDockerfileを使ってVOLUMEの話をするためのサンプルです。

```
# myvol-sampleイメージを作成
$ docker build --tag myvol-sample .
(中略)
Successfully tagged myvol-sample:latest

# myvol-sampleイメージからコンテナを作成し /myvol をボリューム myvol-sample-volume として作成
# コンテナは不要なので --rm をつけて削除しておく
$ docker run --rm --volume myvol-sample-volume:/myvol myvol-sample

$ docker volume ls
DRIVER               VOLUME NAME
local                myvol-sample-volume
```

こうして作ったボリュームには `/myvol/greeting` ファイルが永続化されているため、新たなコンテナからファイルシステムとして使うことができます。

```
# 作成したボリュームを参照するコンテナを走らせる
$ docker run --rm --volume myvol-sample-volume:/myvol busybox cat /myvol/greeting
hello world

# コンテナ上のパスは置き換えることができる (myvol -> myvol2)
$ docker run --rm --volume myvol-sample-volume:/myvol2 busybox cat /myvol2/greeting
hello world
```

## DockerfileでVOLUMEを指定しているイメージからコンテナを作ると無名のボリュームが作成される

先程紹介したDockerfileでは `VOLUME /myvol` を指定して、どこのパスをボリュームのマウントポイントとするかを指定していました。

```Dockerfile
FROM busybox
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol
```

ここからボリュームを指定せずにコンテナを作るとコンソールには何も出ないのですが、実はコンテナ  **だけではなく** ボリュームも作成されます。

```console
# myvol-sampleイメージを作成
$ docker build --tag myvol-sample .
(中略)
Successfully tagged myvol-sample:latest

# myvol-sampleからhogeコンテナを作成
$ docker run --name hoge myvol-sample pwd
/

# 無名のボリュームが作成されている
$ docker volume ls
DRIVER               VOLUME NAME
local                bfef8a28faa92f94c5256b8c5cffee8cd94e245005929cbf8ef7e0af8c60fca1
```

怖いのがこれ、 **コンテナを消してもボリュームは残ります。**
まあDockerのボリュームがコンテナが消えても永続化することを目的としているので消えないのは当然なのですが、ボリュームに名前をつけておかないとなんだかわからないゴミです。

一応 `docker volume prune` で現在動いているコンテナから使われてないボリュームをまとめて削除することもできます。
ただし、たまたま止めてるコンテナが使ってるかもしれないので安易な `docker volume prune` 実行は危険です。

基本はボリュームが作られるコンテナを作るときにはボリュームに名前をつけるようにしましょう。
もしくは `docker run --rm` でコンテナを作るとコンテナおよび自動生成されたボリュームも削除してくれるので活用するといいと思います。

筆者は自動生成されるボリュームにもコンテナと同様にデフォルトの名前がつくといいと思うんですけどどうでしょうか。 `<コンテナ名>-volume1` (連番) とかになっていればどのイメージ・コンテナ関係のボリュームだったかに当たりがつくのではないでしょうか。

## ボリュームのバックアップ・リストアはコンテナを介して行う

ボリュームはバックアップとリストアができます。

***ただしDockerが必要です。***

- Dockerのボリュームのバックアップ
  - 新しいコンテナを作り、bindボリュームでカレントディレクトリを/backupとしてマウント、dbstoreボリュームをtarでホストのファイルシステムに保存
- Dockerのボリュームのリストア
  - ホストシステムからbindボリュームを使ってtarをマウントし、ボリュームをマウントした位置にtarを展開する

↓はDocker本家のドキュメントでの[ボリュームのバックアップ・リストアの手順](https://docs.docker.com/storage/volumes/#backup-restore-or-migrate-data-volumes)として掲載されているものです。

```console
# バックアップ
$ docker run --rm --volumes-from dbstore -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata

# リストア
$ docker run -v /dbdata --name dbstore2 ubuntu /bin/bash
$ docker run --rm --volumes-from dbstore2 -v $(pwd):/backup ubuntu bash -c "cd /dbdata && tar xvf /backup/backup.tar --strip 1"
```

これは泥臭いですね。
ファイルシステムをtarで出力できる `docker export` があるんだからボリュームのexport/importもコマンド一発でできてもいいような気がするんですがそうなってないのはなんでなんでしょうかね？

## コンテナを作らずにボリュームを作ることはできない

[docker volume createコマンド](https://docs.docker.com/engine/reference/commandline/volume_create/) ではからのボリュームを作ることができますが、イメージから直接作ることはできないようです。

docker-composeではトップレベルに `volumes` でコンテナが利用するボリュームを定義できますが、`bind` ではなく `volume` タイプのボリュームをイメージから生成して使用する場合は（ややこしい・・・）、docker-compose upでは作成できないため事前に空コンテナを作ってボリュームを作っておく必要があります。

筆者はdocker-composeででかいデータを持ったイメージとGolangの単一ファイルだけのイメージを組み合わせてWebサーバーを立てたかったのですが、データイメージからボリュームを作ることが `docker-compose up` 一発ではできないことに気づくのにえらく時間がかかりました。

## コンテナ＆ボリューム作成時に存在してないパスを指定すると、そのパスに空のディレクトリを持つボリュームができる

ちょっと罠っぽいなと思ったのでこれも紹介。

docker run時に `--volume <ボリュームの名前>:<コンテナ内でのパス>` を指定することで新規ボリュームが作成できますが、
コンテナにそのパスが存在しなくても普通に空のディレクトリがマウントされたボリュームが作成されます。

```console
# イメージにない /doesnotexists を指定して myvol-sample-volume ボリュームを作成
$ docker run --rm --volume myvol-sample-volume:/doesnotexists myvol-sample ls -al /doesnotexists
total 8
drwxr-xr-x    2 root     root          4096 Dec 29 07:23 .
drwxr-xr-x    1 root     root          4096 Dec 29 07:23 ..

$ docker run --rm --volume myvol-sample-volume:/sample busybox ls -al /sample
total 8
drwxr-xr-x    2 root     root          4096 Dec 29 07:23 .
drwxr-xr-x    1 root     root          4096 Dec 29 07:23 ..
```

ボリュームへの書き込みパスを指定できるので意味はわかるのですが「ディレクトリがあるときはボリュームに格納され、ないときは新規ディレクトリが作られる」は分かりづらい気がしました。

## docker run時に引数 `--read-only` をつけてもマウントしたボリュームには書ける

docker runの `--read-only` オプションの意味は「コンテナのファイルシステムを読み込み専用とする」ですが、マウントしたボリュームは例外で、書き込みが可能です。

コマンドのヘルプでは正直わかりづらいのですが、[docker runの `--read-only` の説明](https://docs.docker.com/engine/reference/commandline/run/#mount-volume--v---read-only)にはちゃんと書いてあります。

なお、筆者はそもそもこの記事を書くまで `--read-only` の存在を知りませんでした。

## ボリュームを参照するときに読み込み専用にするか指定できるが、ボリューム自体が読み込み専用かは設定できない

ボリューム指定時のdestの後ろに `:ro` をつけてreadonlyを宣言すると、書き込み不可になります。

```console
$ docker run --rm --volume=my-volume:/hoge:ro busybox sh -c 'echo "hello" > /hoge/fuga'
sh: can't create /hoge/fuga: Read-only file system
```

ですが、ボリューム自体が読み込み不可になるわけではなく、あくまでコンテナから見て読み込み専用にするかどうかを指定できるだけです。

```console
# /hogeをもつボリュームmy-volumeを作成
$ docker run --rm --volume=my-volume:/hoge:ro busybox

# /hogeをマウントして /hoge/fuga を作成
$ docker run --rm --volume=my-volume:/hoge busybox sh -c 'echo "hello" > /hoge/fuga'
$ docker run --rm --volume=my-volume:/hoge busybox cat /hoge/fuga
hello
```

## docker runでのボリューム指定の引数には `-v/--volume` と `--mount` がある（後者が推奨されている）

私を含めて結構前からDockerを使っている人ほど知らないような気がするんですが、 `docker run` でボリュームを指定するには `--volume` (省略形 `-v`) と `--mount` があります。

[-vと--mountのちがい](https://docs.docker.com/storage/volumes/#differences-between--v-and---mount-behavior) は docker serviceやvolume pluginを使わないときにはほぼ同じことが指定できるのですが、
[docker runの--mountの説明](https://docs.docker.com/engine/reference/commandline/run/#add-bind-mounts-or-volumes-using-the---mount-flag) では --mount を推奨しています（-vがdeprecatedというわけではない）。

にもかかわらず2018/12/29現在 [Docker run reference](https://docs.docker.com/engine/reference/run/) では `--mount` 引数には触れておらず気づきづらいです。

## ボリュームのマウント先にディレクトリが既にあると無警告でディレクトリ内のファイルがすべて書き換わる

これも最初何が起きたかわからなかったのですが、既存のボリュームがもつファイルと同じパスに別のファイルがあると、ボリュームをマウントしてのコンテナ実行時にマウント先のディレクトリはボリューム内のディレクトリで上書きされます。

先述のボリュームを作るイメージのサンプルでは、 `/myvol/greeting` を書き込んでいました。

```console
# myvol-sample-volume ボリュームには /myvol/greeting がある
$ docker run --rm --volume myvol-sample-volume:/myvol busybox ls /myvol
greeting
```

`/myvol` に別のファイル `/myvol/test` をもつイメージ `myvol-sample2` を作成します。このイメージからコンテナを作る際に `/myvol` にボリュームをマウントすると `/myvol/test` が見えなくなります。

```Dockerfile
FROM busybox
RUN mkdir /myvol
RUN echo "test" > /myvol/test
```

```console
$ docker build --tag myvol-sample2 .

# ボリュームを /myvol にマウントすると testファイルが見えなくなる
$ docker run --rm --volume myvol-sample-volume:/myvol myvol-sample2 ls /myvol
greeting

# イメージにファイルは残っているので、ボリュームを使わない or 別のディレクトリにマウントすれば test が残っている
$ docker run --rm myvol-sample2 ls /myvol
test
```

これもGolangアプリと同じディレクトリにデータを配置したく、ボリュームをアプリと同じディレクトリにマウントしたときに気づきました。ボリューム内にあるファイルと名前が違うファイルはマウント後も見えるのかなと思ったんですけど難しいですね。。。

# 終わりに

ここで紹介した他にも [Docker desktopでmacOSでのボリュームのIO速度とホストへの反映速度のトレードオフを選べるcached/delegatedオプション](https://docs.docker.com/storage/bind-mounts/#configure-mount-consistency-for-macos) の話とか、[sshfsなどを使ってホスト外にDocker Volumeを作ってマウントする](https://docs.docker.com/storage/volumes/#use-a-volume-driver)話とかを紹介したかったんですが、今回はこのへんで。

なにか思いついたら追記します。

# 参考資料
- [Use volumes](https://docs.docker.com/storage/volumes/)
- [Manage data in Docker / Choose the right type of mount](https://docs.docker.com/storage/#choose-the-right-type-of-mount)
- [Docker run reference](https://docs.docker.com/engine/reference/run/)
- [CLIs / docker run](https://docs.docker.com/engine/reference/commandline/run/)