Edited at

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命令でどのパスをボリュームとしてマウントできるようにするか定義できます。

FROM busybox

RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol

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

# 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 を指定して、どこのパスをボリュームのマウントポイントとするかを指定していました。

FROM busybox

RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol

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

# 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本家のドキュメントでのボリュームのバックアップ・リストアの手順として掲載されているものです。

# バックアップ

$ 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コマンド ではからのボリュームを作ることができますが、イメージから直接作ることはできないようです。

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

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


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

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

docker run時に --volume <ボリュームの名前>:<コンテナ内でのパス> を指定することで新規ボリュームが作成できますが、

コンテナにそのパスが存在しなくても普通に空のディレクトリがマウントされたボリュームが作成されます。

# イメージにない /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 の説明にはちゃんと書いてあります。

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


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

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

$ 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

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

# /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のちがい は docker serviceやvolume pluginを使わないときにはほぼ同じことが指定できるのですが、

docker runの--mountの説明 では --mount を推奨しています(-vがdeprecatedというわけではない)。

にもかかわらず2018/12/29現在 Docker run reference では --mount 引数には触れておらず気づきづらいです。


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

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

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

# 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 が見えなくなります。

FROM busybox

RUN mkdir /myvol
RUN echo "test" > /myvol/test

$ 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オプション の話とか、sshfsなどを使ってホスト外にDocker Volumeを作ってマウントする話とかを紹介したかったんですが、今回はこのへんで。

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


参考資料





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