DockerのVolume機能について実験してみたことをまとめます

  • 85
    いいね
  • 0
    コメント

Dockerのvolume機能について調べたことをまとめます

Volume

dockerでrunコマンドコンテナを起動するときに「-v」オプションをつけることで 起動元(docker runを実行したホスト)のディレクト コンテナを実行しているホストのディレクトリ 1 を、コンテナ内のディレクトリとしてマウントすることができます。
このマウントされた領域をVolumeというそうです。

% docker run -v /host/path:/container/path some_image
→ ホストの /host/path を、コンテナの /container/path にマウント

このマウント機能により、ホストとコンテナとの間でファイルのやりとりをすることができます。また、コンテナ上のファイルはコンテナを削除すると消えてしまうので、重要なデータはホスト側に保管するために利用します。

このvolumeの場所はdocker runで指定するものであるため、1度コンテナとして起動した後は変更できないようです。

この時のvolumeはuid=1000でマウントされます。gidはuid=1000のユーザの設定次第のようです。他の記事でgid=1000との記載がありましたが、私の手元ではgid=500(staff)でした。dockerではコンテナ上で通常利用するユーザをuid=1000とするのが一般的らしいので、それに則っているのかと思います。
また、ファイルのアクセス権限はホスト上の状態に従っているようす。

# マウント初期状態
% find . -exec ls -dln {} \;
mode        - uid gid  size date       path
drwxr-xr-x  4 501  20  136  1  3 15:05 .
drwxr-xr-x  3 501  20  102  1  3 15:05 ./dir1
-rw-r--r--  1 501  20  10   1  3 15:05 ./dir1/file2.txt
-rw-r--r--  1 501  20  5    1  3 15:05 ./file1.tx

% docker exec cont1 find /cont1/path -exec ls -lnd {} \;
drwxr-xr-x 1 1000 50 136 Jan  3 06:05 /cont1/path
drwxr-xr-x 1 1000 50 102 Jan  3 06:05 /cont1/path/dir1
-rw-r--r-- 1 1000 50 10  Jan  3 06:05 /cont1/path/dir1/file2.txt
-rw-r--r-- 1 1000 50 5   Jan  3 06:05 /cont1/path/file1.txt


# ホスト側でパーミッションやオーナーを変更
% chmod 0777 file1.txt
% sudo chown root ./dir1/file2.txt
% find . -exec ls -dln {} \;
drwxr-xr-x  4 501  20  136  1  3 15:05 .
drwxr-xr-x  3 501  20  102  1  3 15:05 ./dir1
-rw-r--r--  1 0    20  10   1  3 15:05 ./dir1/file2.txt
-rwxrwxrwx  1 501  20  5    1  3 15:05 ./file1.txt

% docker exec cont1 find /cont1/path -exec ls -lnd {} \;
drwxr-xr-x 1 1000 50 136 Jan  3 06:05 /cont1/path
drwxr-xr-x 1 1000 50 102 Jan  3 06:05 /cont1/path/dir1
-rw-r--r-- 1 1000 50 10  Jan  3 06:05 /cont1/path/dir1/file2.txt
-rwxrwxrwx 1 1000 50 5   Jan  3 06:05 /cont1/path/file1.txt
→ パーミッションのみが変更され、オーナーは変わらず


# コンテナ側でパーミッションやオーナーを変更
% docker exec cont1 chmod 0400 /cont1/path/file1.txt
% docker exec -u root cont1 chown games /cont1/path/dir1/file2.txt
% docker exec cont1 find /cont1/path -exec ls -lnd {} \;
drwxr-xr-x 1 1000 50 136 Jan  3 06:05 /cont1/path
drwxr-xr-x 1 1000 50 102 Jan  3 06:05 /cont1/path/dir1
-rw-r--r-- 1 1000 50 10  Jan  3 06:05 /cont1/path/dir1/file2.txt
-r-------- 1 1000 50 5   Jan  3 06:05 /cont1/path/file1.txt

% find . -exec ls -dln {} \;
drwxr-xr-x  4 501  20  136  1  3 15:05 .
drwxr-xr-x  3 501  20  102  1  3 15:05 ./dir1
-rw-r--r--  1 0    20  10   1  3 15:05 ./dir1/file2.txt
-r--------  1 501  20  5    1  3 15:05 ./file1.txt
→ パーミッションのみが変更され、オーナーは変わらず

ただ、このあたりのVolumeの権限は少しややこしいようなのでハマるポイントになりそうです。以下の記事をみると、イメージの元にした環境によっては動作が異なることがあるようです。

また、マウントする時のuid/gidを指定できるようにしようとする動きもあるようです。

Dockerfileでのvolumeコマンド

dockerのイメージを作成するDockerfileには、Volumeというコマンドがあります。このコマンドでもvolumeを設定することができます。

Dockerfile
FROM ubuntu
RUN mkdir /myvol
RUN echo "hello world" > /myvol/greeting
VOLUME /myvol

上記のDockerfileのように記述することで、コンテナ内の /myvol ディレクトリをvolumeとして指定します。例えばDBサーバーをdockerで構築する時にデータの保管領域をコンテナ内に持たないようにするために使用したりするようです。

では、このVolume指定されたディレクトリは、実際にはコンテナ外のどこにマウントされるのでしょうか?

このDockerfileで作成したイメージをdocker runで「-v」オプションなしで起動した場合には、dockerコンテナを稼働させているdockerサーバー内のパスが自動で割り当てられます。dockerサーバーには以下の場所にコンテナ用の作業ディレクトリが用意されているようで、そこにVolumeが自動でマウントされます。

/mnt/sda1/var/lib/docker/volumes/{volume ID}/_data

このVolumeのマウント状況はdocker inspect {コンテナID}コマンドで確認できます。

% docker inspect jenkins
()
  "Mounts": [
      {
          "Source": "/mnt/sda1/var/lib/docker/volumes/03eefde0f6df910352a540c461204a5dca07d9bc93302424b16ca3e12ead0814/_data",
          "Destination": "/dir1",
          "Mode": "",
          "RW": true
      }
  ],
()

先のDockerfileの例は公式説明でのサンプルなのですが、VOLUME指定の前にディレクトリやファイルを作成しています。感覚的には、マウントしてそこにファイルを作る流れの方が自然な気がします。また、volumeにマウントされる領域に既にファイルが存在している場合はどうなるのでしょうか?

そこで、以下のようなDockerfileでimageを作成して起動してみました。

Dockerfile
FROM ubuntu
RUN mkdir /myvol

# VOLUME前にファイル作成
RUN echo "hello world" > /myvol/greeting

VOLUME /myvol

# VOLUME後にファイル作成
RUN echo "hello hello world" > /myvol/greeting2

マウントするパスを指定しない場合

% docker run -d image1
976d7a337b4c300686286a527ac15210c92a6308eb0c674f060f532aa6d80f75
% docker inspect -f '{{json .Mounts}}' 976d7a337b4c
[{"Name":"f39824c1616441199194fcdee59b9227663ec1f061291e073ca76140f81c4e2c","Source":"/mnt/sda1/var/lib/docker/volumes/f39824c1616441199194fcdee59b9227663ec1f061291e073ca76140f81c4e2c/_data","Destination":"/myvol","Driver":"local","Mode":"","RW":true}]
% docker exec 976d7a337b4c30 ls -l /myvol
total 4
-rw-r--r-- 1 root root 12 Jan  3 07:00 greeting
% docker-machine ssh default sudo ls -l /mnt/sda1/var/lib/docker/volumes/f39824c1616441199194fcdee59b9227663ec1f061291e073ca76140f81c4e2c/_data
total 4
-rw-r--r--    1 root     root            12 Jan  3 07:00 greeting

volume領域の中は、VOLUMEコマンド前に作成していたgreetingはありますが、後で作成したgreeting2はありませんでした。
おそらく、VOLUMEコマンドの時点で対象のパスが特殊な状態に固定されているようです。その上で、コンテナ起動時にVOLUMEをマウントした際に、この固定した内容を実際にマウントした領域にコピーするような動きをしているようです。

存在しないパスをマウントしようとする場合

% ls -ld sample2
ls: sample2: No such file or directory
% docker run -d -v $(pwd)/sample2:/myvol image1
4a931d229d2e654ce993cd92c2de4a67342f18120c718910e4a4e582795ac723
% docker inspect -f '{{json .Mounts}}' 4a931d229d2
[{"Source":"/dev/docker/sample2","Destination":"/myvol","Mode":"","RW":true}]
% ls -l
total 88
drwxr-xr-x  2 namu  staff     68  1  3 16:10 sample2/
% ls -l sample2
% docker exec 4a931d229d2 ls -l /myvol
total 0

「-v」オプション付きでdocker runを実行した時点で、ホスト側にはディレクトリが作成されていました。中身が空になっているということで、実体はホスト側のディレクトリになっています。
-v を指定するとVOLUMEコマンドでの効果を打ち消してしまうようです。

中身が空のパスをマウントする場合

% mkdir sample3
% ls -la sample3
total 0
drwxr-xr-x  2 namu  staff   68  1  3 16:12 ./
drwxr-xr-x  9 namu  staff  306  1  3 16:12 ../
% docker run -d -v $(pwd)/sample3:/myvol image1
20da9f443f81ac012e60f6ba88920e197977d542992ab625dbcf9c95e0ca1160
% docker inspect -f '{{json .Mounts}}' 20da9f443f81
[{"Source":"/dev/docker/sample3","Destination":"/myvol","Mode":"","RW":true}]
% ls -la sample3
total 0
drwxr-xr-x  2 namu  staff   68  1  3 16:12 ./
drwxr-xr-x  9 namu  staff  306  1  3 16:12 ../
% docker exec 20da9f443f81 ls -l /myvol
total 0

ここでも、実体がホスト側の内容になっています。

マウントする領域に既にファイルがある場合

% ls -l sample1
total 8
drwxr-xr-x  3 namu  staff  102  1  3 15:05 dir1/
-r--------  1 namu  staff    5  1  3 15:05 file1.txt
% docker run -d -v $(pwd)/sample1:/myvol image1
d8ea06d78121ef4f37d596766667aa66d61d0a496583454c0826ea9b25c29240
% ls -l sample1
total 8
drwxr-xr-x  3 namu  staff  102  1  3 15:05 dir1/
-r--------  1 namu  staff    5  1  3 15:05 file1.txt
% docker exec d8ea06d7812 ls -l /myvol
total 4
drwxr-xr-x 1 jenkins staff 102 Jan  3 06:05 dir1
-r-------- 1 jenkins staff   5 Jan  3 06:05 file1.txt

もちろん。ここでも。

以上のことから、Dockerfileでのvolumeコマンドはimage外の領域を使うことを指示するコマンドですが、image内で対象のパスにファイルが存在する場合は注意が必要です。

なお、docker runコマンドの「-v」オプションで「:」なしでパスを指定すると、Dockerfileにてvolumeコマンドを使ったことと同じ効果になるようです。

# 先のDockerfileでvolumeコマンドを記述しなかったとした上で以下のようになる
% docker run -v /myvol some_image
% docker inspect -f '{{json .Mounts}}' 20da9f443f81
[{"Source":"/mnt/sda1/var/lib/docker/volumes/0beb488402d2a637ad98a29a414f0b0440efad3b9654a30ca9bc3aa91a44c33e/_data","Destination":"/myvol","Mode":"","RW":true}]
→ コンテナ内の/myvolを後づけでvolumeにする。
  もともと/myvolに入っていたファイルもマウント先にコピーされる

Data volume container

dockerのvolume領域は他のコンテナ間で共有することができます。

# some_imageというイメージを、cont1という名前のコンテナとして起動
docker run --detach -name cont1 -v /host/path:/cont/path some_image

# cont2を起動するときに、cont1のvolumeを共有
docker run --detach --name cont2 --volumes-from cont1 some_image
→ cont2内にも /cont/path でvolumeが作られる。

上記のように「--volumes-from」オプションを使用すると、cont1のvolume領域をそのままcont2でも利用できます。
この時のcont1のように、volumeを共有されることだけを想定したコンテナを「Data volume container」というようです。これは使い方上の名前であって特殊な機能や設定を意味しているわけではなさそうです。
公式ページでもこの程度の説明のみで、実際の活用方法などが記述されていないのですが、 以下のような使い方ができるのかなと想像しています。

(例1)

Dockerfileでvolumeコマンドで指定した領域を複数のコンテナ間で共有したい場合に使います。同じ領域を「-v」オプションでマウントしたいと思っても、その場所をDockerサーバー外から明示するのができないので、この方法しかありません。

(例2)

postgresqlとmysqlとapacheが稼働しているコンテナで、これらのデータ領域を別々にvolumeとしているとします。

% docker run --detach --name cont1 
  -v /host/postgresql:/var/postgresql 
  -v /host/mysql:/var/mysql 
  -v /host/httpd_doc:/var/apache/public 
  serv1

このとき、別のコンテナにて上記のうちのhttp用の領域のみを共有したいとしたときに、以下のようにすると少し想定外の形になります。

% docker run serv1 --name cont2 --volumes-from=cont1
→ これだと、不要なpostgresql、mysql用のvolumeにもアクセスできてしまう

そこで、以下のように複数で共有する領域だけをもつコンテナを起動して、2つのコンテナで共有するとよいのかと思います。

% docker run --detach --name=vol_cont 
  -v /host/httpd_doc:/var/apache/public serv1
% docker run --detach --name=cont1 
  -v /host/postgresql:/var/postgresql 
  -v /host/mysql:/var/mysql 
  --volums-from=vol_cont serv1
% docker run --detach --name cont2 
  --volumes-from=vol_cont serv1

こうすることで、必要な場所のみを共有することができます。

(例3)

2つの異なるホスト間の領域をコンテナを通して共有したいときに、以下のようにすることができるのではないかと思います。

$ ssh host1
user@host1 $ docker run --name=vol_cont -v /host1/path:/val/path serv1
$ ssh host2
user@host2 $ docker run --name=cont1 --volumes-from=vol_cont serv1
→ cont1上の/val/pathの中身は、host1の/host1/pathの領域になるはず

バグ?

このdockerのvolumeについては、まだ謎な動作が含まれているような感じがします。先に紹介した、ベースにするディストリビューションによってvolume領域のオーナーを変更できたり・できなかったりするようなことがあります。

他にも、あるdockerのコンテナの内でdocker run -v /path/to:/path/toコマンドを実行してvolumeをマウントしようとすると、コマンドを実行したコンテナ内のパスではなく、そのコンテナが稼働しているdockerサーバー上のパスがマウントされてしまいます。

% docker run --name cont1 image1
% docker exec -ti cont1 /bin/bash
cont1$ ls -l /home/
total 0
cont1% docker run --detach -v /home:/cont_home image1
d896607cee52a203da75790dc80f401de2916a4b1348c713989d0ba190dbdc37
% docker exec d896607cee5 ls -l /cont_home
total 0
drwxr-sr-x 5 jenkins staff 160 Dec 31 10:41 docker
% docker-machine ssh default ls -l /home
total 0
drwxr-sr-x    5 docker   staff          160 Dec 31 10:41 docker
→ cont1上の/homeではなく、このdockerコンテが稼働している
  ホスト(OSX上のdocker環境で言うと、VirtualBox上で起動している仮想OS)の
  /homeがマウントされる

dockerのjenkinsイメージを使ってjenkinsを稼働し、その中でDocker pluginを使おうとしたのですが、この謎の現象のためにjenkins上のworkspaceをスレーブとして起動したコンテナ内にマウントできずに詰まってしまいました。

まとめ

これで公式ドキュメントや各種記事などでよくわからなかったvolumeの動作を、色々実験して掴むことができました。
このところ、dockerがかなり流行っていて様々なところで利用されているようなので、大筋では安定しているのかと思っていたのですが、試してみると以外と不思議な動きもあるので、なかなか難しい面もありそうです。


  1. 2017/03/03修正 マウントされるディレクトリのホストを勘違いしていました。Docker for Macなどで手元にDockerをインストールしているのであれば 「docker runを実行した場所」でも正しいのですが、Dockerホストが別のサーバーの場合はそのサーバー内のディレクトリが使われます。ちなみにdocker-machineでVirtualBoxを使ったDocker環境の場合は、VirtualBox内に手元のホストの/Usersがマウントされているので「docker runした場所」という状態が成立しています。