Dockerにおけるボリュームのマウント
Dockerを使う際, ホストマシン上のボリュームをコンテナ内にマウントしたい場面が多々あります. 本記事ではDockerにおけるマウントについて深く掘り下げていきます. 本記事にはMacだけにしか適用できない事柄を含みます(私がMacを使っているので). その場合はそうだとわかるように断りを入れることとします.
マウントとは
ボリュームのマウントとは, 平たく言えばホストマシンとコンテナでファイルを同期することですね. たとえばホストマシンの~/work
とコンテナ内の/myapp
を同期させ, 片方でファイルを編集すれば, もう片方でもその変更が確認できる状態になります.
しかし同期させる前のディレクトリ内の状態が異なっている場合, どのように同期した状態までもっていくのでしょうか. たとえばそれぞれのディレクトリ内の状態が以下のようになっていたとしましょう.
~/work └── file1.txt /myapp └── file2.txt
この状態から2つのディレクトリを同期させるとしたら, 次のようになることが期待されるでしょう.
~/work ├── file1.txt └── file2.txt /myapp ├── file1.txt └── file2.txt
つまり2つのディレクトリの和集合を考えたということです. しかし次のような場合はどうでしょう.
# ローカルマシン内で ~/work └── file1.txt $ cat ~/work/file1.txt Hello World. # コンテナ内にて /myapp └── file1.txt $ cat /myapp/file1.txt It's a beautiful day.
ファイル名が同じで内容が異なる場合, 和集合を取る操作ができません. では実際はどのような挙動をするのかというと, 片方がもう片方を上書きします. どちらが上書きする側で, どちらが上書きされる側なのかは, マウントの指定によります. つまりマウントには向きがあるということです. 実はDockerにおいては, ホストマシンがコンテナを上書きする, という方向しかありません(私が知っている例外としてMySQLがありますが, これについては本記事の最後に述べます).
ホストマシンとは
上で, ホストマシンとコンテナでディレクトリを同期させることができると述べました. 注意しておきたいのが, ここでいうホストマシンとはMacのことではありません. Mac上で起動している仮想Linuxマシンなのです. そもそもコンテナというものはLinuxカーネルに使われている技術です. したがってMacでコンテナ技術を使うためには, 間に仮想Linuxマシンをはさまなくてはならないというわけです.
ではMacとコンテナはデータのやりとりができないのかというとそうではありません. 仮想Linuxマシンからは, Mac全体が/Mac
として参照できます. つまりMac全体が仮想Linuxマシンにマウントされているわけですね. 他にもMac上の/Users
が仮想Linuxマシンからも/Users
でアクセスできます.
間に仮想Linuxマシンがあることは覚えておいたほうがよいとは思いますが, 以下の考察ではこれを意識することはあまりないと思います.
Dockerにおけるマウントの指定
Dockerによる環境構築をする際, ボリュームをマウントする方法はDockerfile, Composeファイル, コマンドラインの3つがあります.
1. Dockerfile
書式
ひとつはDockerfile内でVOLUME
命令を書く方法です. この命令では引数としてコンテナ側のディレクトリしか指定することはできません.
VOLUME /myapp
挙動確認
具体的にどのような挙動なのかを確かめてみましょう. まずはDockerfileを用意します.
FROM debian RUN mkdir /myapp && \ echo "Hello, World" > /myapp/file.txt VOLUME /myapp
挙動確認のためのコンテナですから, これだけで十分です. このDockerfileをもとにhello
という名前のイメージを作成し,
$ docker build -t hello .
コンテナを起動すると同時にコンテナ内に入ります.
$ docker run -it hello bash
するとちゃんとDockerfile内で作成したファイルが確認できます(プロンプトは$
で表現しています).
$ cat /myapp/file.txt Hello, World.
さて疑問なのは, /myapp
と同期されているディレクトリはどこにあるのか, ということですね. ホストマシン上のディレクトリを指定していませんから, Dockerが勝手に(自動で?)決めています. これは以下のコマンドを実行するとわかります(コンテナIDは最初の数桁だけでOKです).
$ docker inspect <container ID>
真ん中より下あたりにあるMounts
の項目を見てみます.
"Mounts": [ { "Type": "volume", "Name": "e8e~略~ac2", "Source": "/var/lib/docker/volumes/e8e~略~ac2/_data", "Destination": "/myapp", "Driver": "local", "Mode": "", "RW": true, "Propagation": "" } ],
Source
の値が, マウントされているホストマシン上のディレクトリです. Name
やSource
に見られる長い文字列はコンテナIDではなくボリュームIDですね. さてホストマシン上のディレクトリがわかったので, 中身を覗いてみましょう. ただしホストマシンは仮想Linexマシンであることを忘れないように. 仮想Linuxマシンの実体はMacから見ると以下のパス上に配置されています.
~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/Docker.qcow2
以下のコマンドを実行することにより, この仮想マシンの中に入ることができます.
$ screen Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/tty
そこで,
cat /var/lib/docker/volumes/e82~略~ac2/_data/file.txt
とすれば, たしかにファイルが同期されていることが確認できます. 仮想マシンから抜け出すには,
なぜホストマシンのディレクトリを指定できないのか.
Dockerfileにボリュームのマウントを設定する際は, ホストマシンのディレクトリを指定することができません. 理由が公式ドキュメントに書かれています.
The host directory is declared at container run-time: The host directory (the mountpoint) is, by its nature, host-dependent. This is to preserve image portability, since a given host directory can’t be guaranteed to be available on all hosts. For this reason, you can’t mount a host directory from within the Dockerfile. The VOLUME instruction does not support specifying a host-dir parameter. You must specify the mountpoint when you create or run the container.
Dockerを使って環境構築をする理由は, どこでも誰でも同じ環境が手に入れられるからですね. それなのにDockerfileにホストマシン上のディレクトリを指定してしまったらどうでしょう. そのDockerイメージのユーザーは同じディレクトリ構成を持っていなければならなくなります. それよりは, イメージの各ユーザーがコンテナを起動する際に, ホストマシン上のディレクトリを指定するほうがいいだろうという考え方です. 理にかなっていますね.
2. Composeファイル
書式
Composeファイルではホストマシンのディレクトリも指定することができます.
volumes: - ./work:/myapp
各パスは以下のルールに則ります.
挙動確認
挙動をいくつかのパターンで試してみましょう. まずは以下のような構成を用意します.
docker_test/ ├── docker-compose.yml └── test/ └── file.txt
$ cat test/file.txt Hello, World.
例1
挙動確認に必要最低限なComposeファイルを用意します.
version: '3' services: web: image: debian volumes: - ./test:/myapp
以下のコマンドにより, コンテナを起動し, コンテナの中に入りましょう.
$ docker-compose run web bash
web
はComposeファイル内で定義したサービス名, bash
はコンテナの中で実行するコマンドです. コンテナの中に, マウントされたファイルがあることが確認できます.
$ cat /myapp/file.txt Hello, World.
/myapp
というディレクトリも自動的に作られていることもわかりますね.
例2
次はホストマシンのディレクトリの書き方を少し変えてみましょう.
version: '3' services: web: image: debian volumes: - test:/myapp
さっきと同じコマンドでコンテナの中に入ろうとすると, エラーが出ます.
$ docker-compose run web bash ERROR: Named volume "test:/myapp:rw" is used in service "web" but no declaration was found in the volumes section.
「test:/myapp:rw
という名前のボリュームがweb
の中で使われているが, そんな名前をしたボリュームは見つからない」という意味ですね. サブディレクトリを指定する場合は./test
のような書き方にしましょう.
例3
次はDockerファイルをComposeファイルと同じディレクトリに作成します.
FROM debian RUN mkdir /myapp && \ echo "It's a beautiful day." > /myapp/anothe_file.txt
コンテナ内でanother_file.txt
というファイルを作成しています. 現在のディレクトリ構成を確認しておきましょう.
docker_test/ ├── Dockerfile ├── docker-compose.yml └── test/ └── file.txt
Dockerファイルを作成したので, Composeファイルも変更しなくてはなりません.
version: '3' services: web: build: . volumes: - ./test:/myapp
この状態でコンテナを起動します.
$ docker-compose run web bash
するとコンテナ内に入ることができるので, Dockerファイルにて作成したファイルがあるかを確認してみます.
$ ls myapp file.txt
anothe_file.txt
は存在せず, file.txt
だけが存在しています. はじめに述べたように, マウントには向きがあるからです. ホストマシンのディレクトリが上書きしてしまいました.
3. コマンドライン
コマンドラインからも
$ docker run -v ./test:/myapp -it <image> bash
のようにボリュームを指定することができます. また今後詳しく書きたいと思います.
MySQLにおけるマウント
MySQLはデータを/var/lib/mysql
というディレクトリに保存します. このディレクトリをローカルマシンにマウントすることで, データを「永続化」することができます. つまりコンテナを削除してもデータはローカルマシンに残せるようになります.
たとえばdocker-compose up
で起動させたプロジェクトは, docker-compose down
で停止することができますが, この際コンテナも一緒に削除されるようになっています(その理由などはこちらで詳しく述べています). しかしボリュームをマウントしておけば, テーブルや, テーブルに挿入したレコードなどを消えずに残ったままにしておけます.
これまで, Dockerにおけるマウントの向きは一方向に限られるということを述べてきました. しかしMySQLの公式イメージにおいては,
# docker-compose.ymlの一部 service: db: volumes: ./db/data:/var/lib/mysql
のように指定したとしても, /var/lib/mysql
が./db/data
により上書きされることはありません. 実際コンテナ起動後に./db/data
を見てみると, MySQLのデータが確認できます.
MySQLの公式イメージを使う場合は, データの上書きを心配する必要はないということです.