須通り
Sudo Masaaki official site
For the reinstatement of
population ecology.

マウント先のファイルが一時的に見えなくなるけど、マウント解除すると何事もなかったように戻っているアレ、宇宙法則の裏側を垣間見ているみたいで怖いです。

ホーム | 統計 Top | Docker おぼえがき(02)データの保持

Docker おぼえがき(01)基本操作」で基本操作のコマンドを一通り解説したので、このページには Docker コンテナでデータを保持し、ホスト環境との間で収受する方法についてまとめておく。

一口にデータといっても、コンテナの構造や動作環境、用途に応じて様々な扱いが考えられる。今回はサイトの趣旨に合わせて、データサイエンス・フィールド生物学系のユーザーがローカルマシンを主体に、小~中規模の機械学習環境を構築する場合を考える。なおコード例は Ubuntu 18.04 ホスト上の Docker CE 20.10.6 で確認してある。

目次

Docker におけるデータの利用と保持

ドキュメント「コンテナでデータを管理」を読むと、主なデータ保持手法として

  • データ・ボリューム
  • データ・ボリューム・コンテナ

の 2 つがあると説明されている。一方別のドキュメント「Docker におけるデータ管理」には、ファイルをホストマシン上に保存する方法は、

  • ボリューム
  • バインドマウント

の 2 つと書かれている(正確にはこれに tmpfs マウントが加わる)。コンテナ技術そのものが急成長を遂げて Docker の設計思想も成熟の途上にあるが、個人的にはボリュームやバインドマウントで整理するのが初心者に優しいし、ここ1,2年のネット情報とも整合すると思われる。

実際のところボリューム・コンテナは、ボリュームの技術を流用したデザイン・パターンと解釈できる。また複数の者が関わる開発案件になって初めて生きてくる仕組みなので、このページでは詳しく述べない。

大略すると、データボリュームを保持した最小サイズの(追加アプリをインストールしない)名前付きコンテナを一旦作っておいてから、自分が本来やりたいタスクを実行するコンテナを docker run --volumes-from <データボリュームを保持したコンテナ> <仕事をさせるコンテナ> という構文で起動させる。これにより、データを永続的に保持するコンテナと、タスクを実行するコンテナを分離できて、システムの可搬性を担保しやすいわけだ。

ボリュームによるデータの保持

まずボリュームについて。Data volume とは何かの、厳密な説明はとりあえず迂回する。ボリュームもバインドマウントも、実体はホストのファイルシステム上に存在している。これらをコンテナのディレクトリに、外付けハードディスクのような感覚でマウントすることで、コンテナがデータを保持可能になる。ただしボリュームのファイルは、/var/lib/docker/volumes/ という、明らかに Docker の管理領域内に作成される。ユーザーが Docker 外のファイラからこれを触るべきではない。

ボリュームを作成する方法は複数あるが、大別してコンテナの起動前に docker volume create コマンドで作っておくか、docker run のオプションで -v を使い、起動時に作成する。ただし、ボリュームはコンテナ起動時に作成すること「も」できるが、必ずしも特定のコンテナと紐づいた存在ではない点に注意。どちらの方法で作っても、コンテナとボリュームのライフサイクルは原則、分離している(下に示すように例外はある)。


コンテナ起動時にボリュームを作成する例(名前付きボリューム)

## なるべく軽量なイメージ例として Alpine Linux を起動してみる。
## TestVolume0 という名前のボリュームを作成し、コンテナ上の /opt/testvolume0 ディレクトリにマウントする。
$ docker run --rm -it -v TestVolume0:/opt/testvolume0 alpine:latest /bin/sh

## コンテナのシェルに入ったので、ボリュームの存在を確認してみう。
# pwd # 現在のディレクトリを確認。たぶん / にいるはず
# ls # ルート下に存在するお馴染みのディレクトリたちを確認。
# cd /opt # /opt ディレクトリに移動
# ls # /opt の下に testvolume0 が存在することを確認。
# exit # コンテナを抜ける。

exit に -rm があるのでコンテナは自動で削除される。


で、コンテナが停止&削除されたら TestVolume0 はどうなる?

$ docker volume ls # volume ls は現在の環境に存在するボリュームの一覧

DRIVER    VOLUME NAME
local     TestVolume0

上記ではコンテナは削除ずみだが、TestVolume0 が表示される。
なぜかというと、コンテナとボリュームのライフサイクルが分離しているため。

## あるいは、予め volume を作成してからコンテナを起動する
$ docker volume create TestVolume1
$ docker volume ls

DRIVER    VOLUME NAME
local     TestVolume0
local     TestVolume1

## 未使用のボリュームを全部消す
$ docker volume prune

名前付きボリュームは、特定のコンテナに従属しているわけではないので、実際問題として複数コンテナの起動時、1 つのボリュームを同時にマウントして使うことも可能である。

匿名ボリューム

ただし、ボリュームの命運がコンテナと一蓮托生になるケースも存在する。以下のコード例にある「匿名ボリューム」と、コンテナ起動時の暗示的なボリューム作成を組み合わせた場合である。


コンテナ起動時にボリュームを作成する例(匿名ボリューム)

## 今度は左辺(ボリューム名)なしで作成し、コンテナ上の /opt/testvolume0 にマウント。
$ docker run --rm -it -v /opt/testvolume0 alpine:latest /bin/sh
# exit # コンテナを抜ける。 -rm あるのでコンテナは自動で削除される。

## で、コンテナが停止&削除されたら TestVolume0 はどうなる?
$ docker volume ls # volume ls は現在の環境に存在するボリュームの一覧

DRIVER    VOLUME NAME

今度はコンテナが削除されたタイミングで、TestVolume0 も消える。なぜかというと、「コンテナ作成時に -rm を付けつつ、-v で匿名ボリュームを作ると、コンテナの停止&削除に伴ってボリュームは削除される」という仕様になっているためである。なお、docker volume create で明示的にボリュームを作る際に、--name 引数で名前を指定しないことも可能ではあるが、この操作は厳密には匿名ボリュームを作成しているわけではなく、「create コマンドに命名を丸投げしている」だけである。

なおボリューム名に使える文字は半角英数字、_アンダースコア、.ピリオド、-ハイフンで、冒頭はアルファベットの文字でなければならない。

バインドマウントによるデータの保持

一方バインドマウントは、ホストシステム上の任意の場所に元々ある、ファイルやフォルダをフルパスで指定してマウントし、コンテナから読み書き可能にする操作である。こちらについても、ホスト上に元々存在していないパスを指定することが可能であり、その場合は docker run した段階でホスト上に新規作成される。


コンテナ起動時にホスト上のフォルダをマウントする例

## ホストの /home を、コンテナ上の /opt/hosthome にマウントする。
$ docker run --rm -it -v /home:/opt/hosthome alpine:latest /bin/sh

# cd /opt/hosthome # /opt/hosthome ディレクトリに移動
# ls # /opt/hosthome の内容を確認。
# exit # コンテナを抜ける。 -rm あるのでコンテナは自動で削除される。
  • バインドマウントのホスト側のファイルやフォルダは、絶対パスで指定しなければならない。これは -v の左辺でホスト側ディレクトリを指定するという文法が、ボリュームでも使われていて区別が必要なためである。コンテナ側のディレクトリ指定も基本的には絶対パスが必要。
  • その後 --mount を付けて docker run するというシンタックス(下で説明する)が追加された。こちらはもう少し柔軟に書けるため、新規案件は --mount 表記に移行すべきだろう。
  • -v を繰り返せば、複数のボリュームを追加できる。
  • 基本的には読み書き可能でマウントされる。読み込み専用にしたければコンテナ側の名称末尾に :ro と付ける。
  • コンテナ側の挙動は linux の通常の mount に準ずる。たとえばパス /opt/hosthome が仮にイメージ上で既存だった場合、/home を重複マウントする。この場合、既存の /opt/hosthome の内容は削除されず、マウント解除後に再度アクセスできるようになる。
  • コンテナの起動中、ホスト上の別アプリから、バインドマウント元のディレクトリに変更を加えることも可能。コンテナ側の誤操作や悪意を持ったコードの混入により、ホストシステムを破壊するような書き込みすらできてしまう。

tmpfs マウントによるデータの保持

一応 tmpfs マウントについてもちょっとだけ触れておくと、docker run -v xxx として作成する匿名ボリュームを、-tmpfs フラグに置き換えたものだと説明するのが最も簡単だろう。ただし大きな違いとして、ホストのディスクではなくメモリ上にデータが置かれるようになる(docker stop すると削除される)。ホスト上の他アプリケーションからはアクセスできないので、稼働中のコンテナ内で機密データを一時的に保持する等に使える。なお tmpfs はもともと Unix 系 OS における、一時ファイル保持の一種であるため、現時点では Linux 上で起動する Docker コンテナでしか使えない。

で、どの形式でデータを保持すべきか?

現代的な Docker の慣習では、なるべくバインドマウントよりもボリュームを使うべきであるとされている。理由はいくつかある。

  • コンテナの可搬性を最大限確保すべしという Docker哲学においては、ホストに特定のファイルやフォルダが存在する前提で利用する仕組みであるバインドマウントは、一種の必要悪である。
  • バインドマウントは、ホスト OS のファイルシステムを借りて動作しているため、細かなチューニング(たとえばディスクへの書き込み単位の制御)が苦手であるほか、僅かだが動作が遅くなる。
  • ボリュームであれば保存場所が /var/lib/docker/volumes/ 以下に限定されているので、やんちゃしてもホストのシステムファイルを破壊するリスクを避けられる。

ただしコンテナの可搬性よりも、ローカル作業用のアプリケーションの環境分離ツールとしての利便性を重視する場合には、バインドマウントの優位は明らかである。

  • コンテナで動かしたいアプリの設定ファイルを、ホストに既にインストールされているものに揃えたい場合。
  • 既にローカルマシンの特定の場所にスクリプトやデータがあり、加工や分析だけをコンテナで実施してから、結果をホストに戻したい場合。たとえば機械学習ライブラリの動作環境を Docker image で拾ってくる場合が当てはまる。

個人的には、手元で特定の解析ツールを動かすためのお手軽環境構築ならばバインドマウントを、手元で開発したアプリと付属データをサーバー上に展開して、何らかのサービスに供するならばボリュームを、機密性の高いデータや、ディスクを介さずに超高速で処理したいデータであれば tmpfs を使う、くらいの理解で良いかと思う。

"--mount" オプションベースでのストレージ管理

これは Docker 17.06 から実装された方法で、上で述べたボリューム、バインドマウント、tmpfs マウントのフラグ管理を統一したものである。


従来 -v xxxx:xxxx と書いていた部分を、以下に置き換える。
--mount type=volume|bind|tmpfs,source=ボリューム名ないしホストディレクトリ,target=コンテナ側マウント場所

なお type 以下はキーバリューペア(=)で指定する。
source の替わりに src と書いたり、
target の替わりに destimation や dst と書くこともできる。


## --mount でコンテナ起動時にボリュームをマウントする
$ docker volume create TestVolume2 # --mount のボリュームは起動時に匿名で作れない
$ docker run --rm -it --mount type=volume,source=TestVolume2,target=/opt/testvolume2 alpine:latest /bin/sh
# cd /opt
# ls
# exit 

$ docker volume ls
$ docker volume prune

## --mount でコンテナ起動時にホスト上のディレクトリをマウントする
$ docker run --rm -it --mount type=bind,source=/home,target=/opt/hosthome alpine:latest /bin/sh
# cd /opt
# ls
# exit 


## --mount でコンテナ起動時に tmpfs をマウントする。こいつだけ source キーがなく、インスタントに作成される。
$ docker run --rm -it --mount type=tmpfs,target=/opt/himitsu alpine:latest /bin/sh
# cd /opt
# ls
# exit 

再起動時のボリュームの扱いと削除方法

  • 作成時に -v を付けたコンテナにおいては、 stop しても volume への参照は消えないので start すれば、再び volume が使える。
  • ホストのディレクトリをマウントしたボリュームも、停止→再起動後にマウントが残る。

さて、不要になったボリュームを削除する方法は 3 個ある。

  1. コンテナから参照されていないボリュームは、docker volume prune で消える。
  2. 手動で消すには、volume ls で名を調べてから volume rm <消したいボリューム名>。
  3. あるいはコンテナを削除する際、通常の docker rm <コンテナ名> に -v フラグを足して docker rm -v <コンテナ名> とすると、「コンテナ+ボリューム」を削除できる。

どの現存コンテナからも参照されずに残ったボリュームを「宙づり」(dangling) ボリュームといい、放置するとディスク容量を食う。初期の Docker ではこれを消す方法が整備されていなかったが、2021 年現在では volume prune を使えば一発よ。


$ docker volume create TestVolume3
$ docker run --rm -it --mount type=volume,source=TestVolume3,target=/opt/testvolume3 alpine:latest /bin/sh
# cd /opt/testvolume3
# echo "Hello, world." >foo.txt
# exit # コンテナ停止

上記により、ユーザーによって変更されたボリュームができあがる

消去法 1 volume prune を使う
$ docker volume ls
$ docker volume prune

## たとえユーザーが内容を変更していても、コンテナから参照されないボリュームは prune で刈られる運命にある。

消去法 2 volume rm を使う
$ docker volume ls -f dangling=true
$ docker volume rm TestVolume3

消去法 3  docker rm -v <コンテナ> を使う
$ docker volume create TestVolume4
$ docker run -it --mount type=volume,source=TestVolume4,target=/opt/testvolume4 \
 --name="TC4" alpine:latest /bin/sh
# cd /opt/testvolume4
# echo "Goodbye, world." >bar.txt
# exit # コンテナ停止(削除はされない)
$ docker volume ls
$ docker rm TC4
$ docker volume ls

なおコンテナ起動時に、ホスト側のディレクトリをバインドマウントした場合には、docker rm -v コンテナ名 でコンテナを削除しても、ホスト側のデータを誤って消すことはない。

コミット時のボリュームの扱い

設計思想上、ボリュームはコンテナの中身ではない。そのため docker run -v 付きで作成したコンテナを commit や export すると、ボリュームを含まない形でイメージが書き出される (逆に言うと、コンテナ内のディレクトリにデータを入れれば、書き出したイメージは最新のデータを含むスナップショットである)。