【Docker】ホスト⇔コンテナ間のファイル共有:「名前付きボリューム」と「バインドマウント」の挙動の差異と、変更時の注意点

docker-volume-vs-bind-mount-behavior-and-pitfalls システム開発

Docker にて、ホストとコンテナのファイルを共有する時には、「名前付きボリューム」と「バインドマウント」の2種類がある。
docker compose の内容によっては、この差異を知っておかないとドハマりする事があるので、その事について書いてみた。

なお、以下の解説では、Docker や Docker compose の使い方の解説は省略しています。

<環境>
インフラ:AWS EC2
アーキテクチャ:64ビットArm
OS:Ubuntu Serer 24.04
コンテナ:Docker version 28.1.1
     Docker Compose version v2.35.1

Docker にて、ホストとコンテナのファイルを共有する方式「名前付きボリューム」と「バインドマウント」の差異について

ホストとコンテナのファイルを共有する方式は、2種類存在する。
両者の記述方法は非常に似通っており、ミスや混乱を招きやすい構造となっている。

名前付きボリューム

先頭に「./」を付けない記述

    volumes:
      - mediawiki_data:/var/www/html

共有するディレクトリは、Dockerが管理するディレクトリとなる。
今回のケースだと、パスは、「 /var/lib/docker/volumes/mediawiki_mediawiki_data 」といった、ユーザのホームディレクトリとはかけ離れたパスに管理される。

バインドマウント

先頭に「./」を付ける記述

    volumes:
      - ./mediawiki_data:/var/www/html

共有ディレクトリは、ホスト側が管理するディレクトリとなる。
パスは Dockerfile(または compose.yaml や docker-compose.yaml) が存在するディレクトリの下の階層となる。
例えば、ubuntu ユーザがホームディレクトリで実行した場合、「 /home/ubuntu/MediaWiki/mediawiki_data 」といった感じで、ユーザのホームディレクトリにて管理される。

「共有するディレクトリのパスが違うだけで、どっちも挙動は同じなんじゃないの?」と思いきや、両者は起動時の動作に明確な差がある。

以下、実際に起こった事をベースに解説。

状況

会社で提供しているアプリのマニュアルを、MediaWiki で管理する事になった。
画像やPDFといったファイルをアップロードするが、それらはホストからも簡単にアクセスしてコピーしたりできるようにしたかったので、メディアファイルはホストとコンテナで共有する設定をした。
が、MediaWiki の設定を進めていくと、何かとホストと共有したいファイルが出てきて、そのたびに compose.yaml に共有したいディレクトリを追加し、そのたびにいちいち設定を変更するのが面倒になってきたので、
「もう、コンテナで管理している /var/www/html のファイルをまるっとホストと共有した方が早くね?そうしたら、あとで『あれも必要だった。yamlファイル修正しよう』『これも必要だった。yamlファイルの修正を(略 』とかしなくて済むし、後から何か追加で共有したいファイルが出てきたとしても対応できるでしょ。」
と、コンテナが保持しているファイル(アプリケーションの稼働に必要な部分)の全てをホストからもアクセスできるようにした。

MediaWiki とは?

Wikipedia でおなじみの wiki システム。
こういうアプリ。 

MediaWiki/ja – MediaWiki

オープンソースとなってるので、誰でも自由に Wikipedia のようなサイトを作り、情報を登録・管理する事ができる。
公式のコンテナイメージも配布されている。
この説明においては、MediaWiki が何なのかという事はそこまで重要ではないので、詳細は割愛。

ホストとのファイルの共有方法するcompose.yaml : ver1 (名前付きボリューム)

具体的には、最初は、こんな感じの compose.yaml を書いていた。
(※ホストとコンテナのディレクトリを共有する部分のみに焦点を当ててます)

    volumes:
      - mediawiki_data:/var/www/html

この yaml ファイルは、記述ミスにより意図しない動作をしていた。
具体的には、docker compose up コマンドを実行するディレクトリに、ホストとコンテナのファイルの共有ディレクトリを置きたかったが、先頭に「./」をつけ忘れていたので、意図しないパス( /var/lib/docker/volumes/mediawiki_mediawiki_data )にて共有されてしまった。

これだと、保存したメディアファイルを直接持っていきたい時、ワケわからん階層にアクセスしなければならなくなる。しかもそのディレクトリは root しかアクセスできないというおまけ付きだ。
という事で、ホストとコンテナの共有の部分(volumes: のセクション)を修正した。

ホストとのファイルの共有方法するcompose.yaml : ver2 (バインドマウント)

    volumes:
      - ./mediawiki_data:/var/www/html

差分は先頭に 「./」 を付けた。それだけ。
当然、そのディレクトリには何もファイルが存在しないので、それらのファイルはコンテナからコピーした。
具体的には、こんな感じのコマンドを使用した。

sudo docker run --rm -v mediawiki_mediawiki_data:/data:ro -v "$PWD":/backup alpine sh -c 'cd /data && tar czf /backup/mediawiki_data_backup.tgz .'
mkdir -p ./mediawiki_data
sudo tar -xzf mediawiki_data_backup.tgz -C ./mediawiki_data

これで無事、意図通りに動くようになった。
リポジトリに管理していた compose.yaml ファイルも修正し、適切な状態になった。

別のサーバで適用する時に起こったエラー

この MediaWiki の環境を別のサーバにも作成する必要があったので、修正後の状態の compose.yaml ファイルを使用した。

が、そこで問題が発生した。
稼働させた MediaWiki が全く動かなかった。
何が原因やねんと思って調べてみると、コンテナ内の「/var/www/html」ディレクトリが空っぽ。つまり、アプリケーションの稼働に必要なファイルが全て消え去っていた。

別サーバで起こっていたエラーの原因

以下、compose.yaml の記述内容。バインドマウントを採用している。

    volumes:
      - ./mediawiki_data:/var/www/html

ここでは、「バインドマウント」で記述している。
この内容で、まっさらな状態からこれを実行すると、カレントディレクトリに、「mediawiki_data」という名前の、中身が空のディレクトリが作成される。
そして、バインドマウントの場合、常にホスト側のファイルを正として扱う。
そのため、ホスト側に作成した中身が空のディレクトリで、コンテナ内の内容を上書きするという現象が起こっていた。

ホスト側に該当のディレクトリがあろうがなかろうがお構いなし。
ディレクトリが無ければ、問答無用でコンテナ内のデータをばっさり削除して空のディレクトリで上書きし、ファイルを完全に抹消させる。
何という恐ろしい動きしてくれるんだ。そりゃ、アプリが動かなくなるわ。

「名前付きボリューム」の場合はどうなってたの?
と思い、調べてみた。

以下、名前付きボリュームでの記述。

    volumes:
      - mediawiki_data:/var/www/html

名前付きボリュームの場合、共有ディレクトリは、Docker が内部で管理しているディレクトリとなる。
今回の場合、ホスト側から見たパスは、「/var/lib/docker/volumes/mediawiki_mediawiki_data」となる。(root 権限でしかアクセスできないディレクトリ)

当然、コマンドを実行する前はこんなディレクトリは存在しないので、Docker コマンドを実行した時に自動生成される。
この時、コンテナ側のファイルを共有ディレクトリにコピーする。
つまり、名前付きボリュームの場合、初回実行時はコンテナ側のファイルを正として、ホスト側との共有ディレクトリを作成する。
ちなみに、2回目以降(ホスト側にもディレクトリが作成された後)は、ホスト側を正として動く。

何でこんなややこしい挙動になっているんだ。
どっちも同じ動きにしてくれよ。

最初に環境を作成した時は、間違って「名前付きボリューム」にした後、後で「バインドマウント」に切り替えていたんで、偶然にもこれらの問題を解決したルートを辿っていた。

整理すると、両者にはこういった挙動の違いがある。

  • 「バインドマウント」の場合、常にホスト側のファイルが正となる。ホスト側にディレクトリが無い場合、空のディレクトリを作成してコンテナに上書きする。(結果、コンテナ内のファイルが消える事がある)
  • 「名前付きマウント」の場合、初回実行時はコンテナ側のファイルが正となり、コンテナのファイルをホスト側にコピーする。2回目以降はホスト側のファイルが正となり、ホストの変更はコンテナ側にも変更される。

以下、対応策。

もちろん、上記のように、
「最初に名前付きボリュームでコンテナを起動させ、ファイルのバックアップを取り、コンテナを終了させた後に今度はバインドマウントで起動させ、バックアップしたファイルをコピーする」
という手順を取っても解決できるが、かなり面倒なうえで compose.yaml を書き換えるという作業が発生するためリポジトリ管理が非常にややこしくなってしまうので、別の方法を考えてみた。

対策

「名前付きボリューム」にすると、ホスト側から見た共有ディレクトリが「/var/lib/docker/volumes/mediawiki_mediawiki_data」とややこしいパスになり、何より root でしかアクセスできないというのが煩わしい。
そもそも、Dockerfile が存在するパスとかけ離れた位置にあるのは、運用上非常に良くない。という事で、名前付きボリュームにするという方針はパス。

当然、「バインドマウント」を使用する事になるが、こちらは常にホスト側のファイルが正となってしまい、コンテナ側へ上書きしてしまう。
という事で、あらかじめホスト側でファイルを作成しておく事にした。
具体的には、docker compose初回実行前に、起動してすぐ消えるコンテナを用意し、ファイルだけコピーしておいた。
具体的には、こんな感じ。

# ディレクトリを作成
mkdir ./mediawiki_data

# ファイルをコピー
sudo docker run --rm -u $(id -u):$(id -g) \
  -v "$PWD/mediawiki_data":/out \
  mediawiki:1.43.1 \
  sh -c 'cp -a /var/www/html/. /out/'

<やってる事>
・最初に、ディレクトリ「mediawiki_data」を作成する
・次に、コンテナを起動し、起動時に作成されるデータを、ディレクトリ「mediawiki_data」に放り込んで、すぐに終了

こうやって、ホスト側にコンテナと全く同じファイルを用意し、上書きしても問題無い状態をあらかじめ用意した。
そうする事で、「ホストを正としてコンテナ側にファイルを丸コピーする」という挙動をしても、問題無く動作できるようにした。

まとめ

整理すると、こんな感じ。

  • Docker がホストとコンテナのファイルを共有する方式(docker compose の yaml ファイルの、「volumes:」セクション)には、「名前付きボリューム」と「バインドマウント」の2種類がある。
  • 両者の記述上の差異は非常に小さく、ミスや混乱を誘発しやすい
  • にもかかわらず、挙動では大きな差異がある部分もあり、気づきにくいエラーが発生する事がある。
  • 「バインドマウント」の場合、常にホスト側のファイルが正となる。ホスト側にディレクトリが無い場合、空のディレクトリを作成してコンテナに上書きする。(結果、コンテナ内のファイルが消える事がある)
  • 「名前付きマウント」の場合、初回実行時はホスト側のファイルが正となり、コンテナのファイルがコンテナにコピーする。移行、ホスト側のファイルが正となり、ホストの変更はコンテナ側にも変更される。

ちなみに、ホストとコンテナで共有するディレクトリが、最初は空っぽで、そこにコンテナから作成したファイルを放り込んでいく、という内容なら、これらの挙動の差異を気にする必要は無い。

「名前付きボリューム」と「バインドマウント」の、どちらを使うべきか

調べてみると、割と意見は分かれている。
Docker 公式では「名前付きボリュームを使うといいよ」と書かれている事を根拠に「名前付きボリューム」を推す意見もあるが、これはちょっと危険な気がする。

Docker なんて、トライアンドエラーを繰り返していくうちにゴミデータが累積されたり、「エラーが出たが解決できん。とりあえず、まっさらな状態にして最初からやり直すか。」という事はよくある。
この時、docker rm コマンドに様々なオプションを付けて実行するが、実行内容によっては、Docker が管理するボリュームはコマンド1発で容易に削除される。

もちろん、ファイルが削除されないように細心の注意を払う必要があるのだが、業務データを docker rm コマンドで消せるというのは、精神衛生上、非常に良くない気がしている。
ファイルを消す場合、管理者が意図したパスのファイルを、「このファイルを消す」という明確な意思を持って実行するべきで、「オプション付けて実行したら消えてました」という状態にしておくのは避けた方がいいんじゃないかと思う。

そして、意見が分かれているのも納得できる。
Docker 開発側は「Docker は便利で安全ですぜ。ファイルの管理は Docker が内部で管理している匿名ディレクトリ(名前付きボリューム)に任せておけばOKだ!」と主張したいだろうし、それを根拠に名前付きボリュームを推すのは理解できる。

が、ファイルはサーバ管理者が管理しやすいパスにしておくのが望ましと思うんで、バインドマウントを使うのがいいんじゃないかな。

コメント

タイトルとURLをコピーしました