Dockerのまとめ - コンテナとボリューム編

## この記事で伝えたいこと

- Docker for Mac が使う Dockerの実行環境 moby vm と LinuxKit 
- Docker は Mac 上ではなく hyperkitという 仮想マシン上で動いている(Docker for Mac の場合)
  - Linux で Dockerを使うときは同じカーネルを使うのでMac環境とかなり異なる
- volume は仮想マシン上の /var/lib/docker/volumes 配下においている

## 前回のおさらい

前回はコンテナとイメージ周りのデータ構造を見てきました。イメージは複数の読み取り専用のレイヤで構成され、コンテナはイメージのレイヤをベースに起動し、レイヤとしてcommitするまで永続化されないことを見てきました。では、MySQLのデータのような、コンテナを破棄しても残しておきたいデータはどうしておくとよいでしょうか？

今回は永続化したいデータを保持するためのボリュームと、コンテナが動作している moby vm について見ていきます。

## この記事で解説する docker の概念

- コンテナ
- ボリューム
- moby vm と Linuxkit

## ボリュームの作り方

ボリュームは、例えて言うなら拡張データ領域です。早速作成してみましょう。

```
$ docker volume create test_volume
test_volume
 $ docker volume ls
DRIVER              VOLUME NAME
local               test_volume
```

volumeのサブコマンドはこんな感じ

| docker command | 意味 |
|:-------------|:-----------------|
| docker volume create [volume] | 指定した volume を作成 |
| docker volume inspect [volume] | 指定した volume の設定を見る |
| docker volume ls | 存在する volume を一覧する |
| docker volume prune | 使われていない volume を削除する |
| docker volume rm [volume] | 指定した volume を削除する |

## コンテナからボリュームをマウントして起動する

では、コンテナからマウントして起動してみましょう。

```
$ docker container run --rm --name test -v test_volume:/home -it alpine:latest sh
```

ここで指定しているオプションも簡単に解説します。

| オプション | 意味 |
|:-------------|:-----------------|
| --rm | 終了時にコンテナを破棄する |
| --name [NAME] | コンテナに指定した名前をつける。未指定の場合は適当な名がつく。 |
| -v [volume名]:[path] | pathに volume を割り当てる |
| -i | 対話シェルを開く |
| -t | 仮想ttyを割り当てる |
| sh | コンテナで起動するコマンド |

containerサブコマンド、imageサブコマンドは別途補足でよく使うものを解説します。

## ボリュームにファイルを保存し、永続化できていることを見てみる

先程起動したコンテナの中でファイルを保存し、再度起動して永続化できているのを確認しましょう。

```
/ # cd /home
/home # ls
/home # echo 'Hello volume' > test.txt
/home # ls
test.txt
/home # echo 'Not save on /root' > /root/not_saved.txt
/home # exit
$ docker container run --rm --name test -v test_volume:/home -it alpine:latest sh
/ # ls /root
/ # ls /home
test.txt
```

`/root` に保存したファイルは消えてますが、 `/home` に保存したファイルが残っています。

## ボリュームがどこに保存されているか見ていこう

では、この test_volume はどこにあるでしょう？ `inspect` サブコマンドを使って見ていきます。

```
$ docker volume inspect test_volume
[
    {
        "CreatedAt": "2018-07-01T15:21:33Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/test_volume/_data",
        "Name": "test_volume",
        "Options": {},
        "Scope": "local"
    }
]
```

なるほど、 `/var/lib/docker/volumes/test_volume/_data` ですか。と思っておもむろに `ls` してみましょう。

```
$ ls /var/lib/docker/volumes/test_volume/_data
ls: /var/lib/docker/volumes/test_volume/_data: No such file or directory
```

見つかりません。なぜなら docker は VM で動いているからです。

## screen を使ってmoby vmの仮想ターミナルを開く

下記コマンドを叩いてみましょう。

```
$ screen ~/Library/Containers/com.docker.docker/Data/vms/0/
```

```
linuxkit-025000000001:~#
```

というような文字列が表示されたはずです。おもむろに `ctrlを押したままD` を叩いてみましょう。

```
Welcome to LinuxKit

                        ##         .
                  ## ## ##        ==
               ## ## ## ## ##    ===
           /"""""""""""""""""___/ ===
          {                       /  ===-
           ______ O           __/
                          __/
              ___________/

linuxkit-025000000001 login: root (automatic login)

Welcome to LinuxKit!

NOTE: This system is namespaced.
The namespace you are currently in may not be the root.
login[2738]: root login on 'ttyS0'
linuxkit-025000000001:~#
```

ハロー、クジラさん。ということで LinuxKitが動くVMの中に入れました。先に終了方法を書いておくと `ctrlを押したままA` のあとに `ctrlを押したままK` のあとに `yes` を押してください。

## 先程のvolumeを確認する

試しに先程のパスを見てみましょう。

```
# ls /var/lib/docker/volumes/test_volume/_data
test.txt
linuxkit-025000000001:~# cat /var/lib/docker/volumes/test_volume/_data/test.txt
Hello volume
```

先程書き込んだファイルが見えたはずです。このまま別のセッションを開き、コンテナを起動して test_volume の内容を書き換えると、 moby vm の中のボリュームも更新されます。このように、 volume はコンテナと別の領域にファイルが保存されるため、コンテナの破棄に影響されません。

## コンテナの方はどうなってるの？

では container の方はどうなっているんでしょうか？ inspect サブコマンドで確認しましょう。

```
$ docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
470a088200f6        alpine:latest       "sh"                4 seconds ago       Up 2 seconds                            test
$ docker container inspect test
[
    {
        "Id": "470a088200f693a8b60efe74b59431f99d30cadb9df2b4ebd0d151d24ccd12fe",
        "Created": "2018-07-01T15:59:08.60750946Z",
        "Path": "sh",
        "Args": [],
        ...
        "GraphDriver": {
            "Data": {
                ...
                "MergedDir": "/var/lib/docker/overlay2/13808d33031c2e5b79ca99c346b4a5d0e1c84c2bc82c25a26572b6cdfd7f313e/merged",
                ...
            },
            "Name": "overlay2"
        },
        "Mounts": [
            {
                "Type": "volume",
                "Name": "test_volume",
                "Source": "/var/lib/docker/volumes/test_volume/_data",
                "Destination": "/home",
                "Driver": "local",
                "Mode": "z",
                "RW": true,
                "Propagation": ""
            }
        ],    }
]
```

細かいことはおいておいて、JSONが返ってきて、先程マウントしたボリュームについての情報も表示されているはずです。

## コンテナ側の設定を見てみる

コンテナ側の設定を見てみるのも面白いです。試しに `GraphDriver/Data/MergedDir` にあるパスをmoby vm上で覗いてみましょう。

```
linuxkit-025000000001:~# ls /var/lib/docker/overlay2/13808d33031c2e5b79ca99c346b
4a5d0e1c84c2bc82c25a26572b6cdfd7f313e/merged
bin    etc    lib    mnt    root   sbin   sys    usr
dev    home   media  proc   run    srv    tmp    var
```

起動している test コンテナのルートディレクトリが見えます。試しに `root` ディレクトリの中を見てみましょう。

```
linuxkit-025000000001:~# ls /var/lib/docker/overlay2/13808d33031c2e5b79ca99c346b
4a5d0e1c84c2bc82c25a26572b6cdfd7f313e/merged/root
not_saved.txt
```

最初に起動したコンテナを終了していなければ、追加した `not_saved.txt` ファイルがあるはずです。

## 結局のところコンテナとは何なのか？

Linuxには chroot というコマンドがあります。このコマンドは実行したプロセス、及びその子プロセスのルートディレクトリを変える、というものです。 docker は chroot や カーネルの機能である namespace 、 cgroup なんかを使って環境を独立させたものがコンテナです。
ちなみに Windows Server にも類似の機能があり、 Windows 用のコンテナもあります。

## (補足) ローカルのファイルシステムをマウントする bind マウント

（Docker for Windows を使ったことがないので Docker for Mac に限った話かもしれません。）

[docker volume のマウント方式にはローカルのファイルシステムをマウントする bind マウントという方式があります。](https://docs.docker.com/storage/bind-mounts/)この方式は確かにローカルのファイルシステムをマウントできますが、LinuxKitからMacのファイルを参照できるよう、LinuxKitへ転送しています。このプロセス `com.docker.osxfs` がMac上のプロセスとしてメモリ食いであり、重いです。

[bind マウントが遅いため、それを回避する docker-syncというツールもあります。](https://github.com/EugenMayer/docker-sync)しかし、このツールは、直接 docker container がbindしているvolumeを参照しないことでコンテナ自体を高速化するためのもので、Mac->LinuxKitへの転送自体が早くなるものではありません。

## まとめ

ボリュームとコンテナについての機能解説と、moby vm と LinuxKit の中身まで見てきました。docker が Linux の機能を積み重ねて出来ているところもざっくり見ました。 

Mac上でdockerコマンドを叩くと moby vm に通信するあたりとかも面白かったりするんですが、それは別の機会で。

## よく使う conteiner のサブコマンド

| docker command | 意味 |
|:-------------|:-----------------|
| docker container run [image] | 指定した イメージ を元に起動 |
| docker container ls (-a) | 存在する コンテナ を一覧する。-a を着けると停止した コンテナ も表示する |
| docker container stop [container_ID] | 指定した コンテナ を停止する |
| docker container prune | 使われていない コンテナ を削除する |
| docker container commit [container] | 指定したコンテナからイメージを作成する |

## よく使う image のサブコマンド

| docker command | 意味 |
|:-------------|:-----------------|
| docker image build PATH | 指定した Dockerfile を使ってイメージを作成する |
| docker image ls  | 存在するイメージを一覧する |
| docker image history [image_ID] | 指定したイメージの作成履歴を表示する |
| docker image prune | 使われていないイメージを削除する |
| docker image rm [image_ID] | 使われていないイメージを削除する |
| docker image commit [container] | 指定したコンテナからイメージを作成する |
| docker image pull [image_ID] | 指定したイメージを取得する |
| docker image push [image_ID] | 指定したイメージをpushする |
| docker image tag [image_ID] [target] | 指定したイメージにタグをつける |

## 参考文献

- [docker for Mac で動くmoby linux にアクセスする](https://dev.classmethod.jp/server-side/docker-server-side/access-moby-linux-with-docker-for-mac/)
- [Getting a Shell in the Docker for Mac Moby VM](https://gist.github.com/BretFisher/5e1a0c7bcca4c735e716abf62afad389)