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の値が, マウントされているホストマシン上のディレクトリです. NameSourceに見られる長い文字列はコンテナ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

とすれば, たしかにファイルが同期されていることが確認できます. 仮想マシンから抜け出すには, +Aを押したあとにKを押します. 確認ダイアログがでてくるので, Yを押してください. これで抜けられます.

なぜホストマシンのディレクトリを指定できないのか.

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の公式イメージを使う場合は, データの上書きを心配する必要はないということです.