DockerのVOLUMEを理解してdocker,docker-compose on WSLのパス問題をエレガントに解決しよう

## TL;DR

Q. docker、docker-composeのボリューム指定がWSLで上手く動かないんだけど？
A. シンボリックリンクを作ればいいよ。ほれっ↓（→ え？どこに作ってるの？）

**(試す場合は自己責任で)**

```sh
# WSL上で実行する または .bashrc などに書く
docker run -i --rm -v /:/WSL alpine ln -snf /c /WSL/mnt/c

# 参考 WSL上の /home/$USER/directory から /mnt/c/Users/ユーザー名/directory に
# シンボリックリンクを貼るという使い方をしてる人
docker run -i --rm -v /:/WSL alpine ln -snf /c/Users/ユーザー名 /WSL/home/$USER
```


この記事の方法を使うとdockerでもdocker-composeでもLinuxとまったく同じようにボリュームのパス指定ができて、シンボリックリンクが絡んだディレクトリでも問題なく動きます（さすがにWindowsから見えないWSL上のディレクトリは無理ですが）。さらにWSLの設定は変えません。他では見かけない新しいやり方・・・かどうかは知りませんが、少なくとも私が考えた方法です。

おまけでDockerのボリュームの雑な解説記事になってます。（おまけなので正確性は求めないでください。）

## 長い解説

### VOLUMEの仕組み

Dockerのボリュームってわかりづらくないでしょうか？データを永続化する仕組み？指定したディレクトリをコンテナ内にマウントしている？これは間違ってはおらずボリュームで一番多い使い方だと思いますが、ここを出発点とすると逆に理解するのが難しくなると思います。

思いっきり適当ですが、次の順番で理解していきましょう。（前提としてDockerはクライアントサーバーモデルというのは知っていますよね？）

1. ボリュームとはDockerサーバーがコンテナに割り当てるストレージ領域である。
2. （DockerfileでVOLUMEが定義されてる）コンテナを作成するとそのコンテナ専用にストレージが割り当てられる。
3. このストレージの保存ディレクトリの場所はDockerサーバーが勝手に決める。
（当然サーバー上のディレクトリ）
4. コンテナを削除してもボリュームは残り続けるので後からでもデータを回収できる。（少しめんどくさいですが）
5. コンテナを作らずにボリュームだけを作成することもできる。そのときに任意の名前をつけることもできる。
6. コンテナを起動するときに既存のボリュームを割り当てることができる。（docker runの-vオプション）
7. Dockerサーバーが作ったどこかのディレクトリのボリュームの代わりに、ユーザーが指定した（サーバー上の）ディレクトリをボリュームとして利用（代用？）できる。

普段よく使われてる（？）使い方は最後の7.のパターンというわけです。

### VOLUMEはサーバー上のディレクトリをマウントしている

（この記事で散々、ボリューム＝ディレクトリのような書き方をしていますがファイルも指定できます。）

さて本題の前に、VOLUMEで勘違いされそうな点を明確にしておきます。それはクライアントのディレクトリの中身をサーバーに送り込んでいるのではないということです。ボリュームはクライアントにはありません。サーバー上にあります。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54918/63ceedc8-baff-890d-4313-0370f5f479c4.png)
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54918/d1b1e590-3fcb-0407-b58d-736c96201a83.png)

`docker build`コマンドがクライアントにあるファイルをサーバーに送り込んでビルドしているのとは異なり、ボリュームではパスだけを渡してます。

### Linuxではどう見えるか

Linuxでは一般的にDockerクライアントもDockerサーバーも同一のマシン上にあって（仮想マシンも使いません）クライアントのディレクトリのパスも、サーバーのディレクトリのパスも同じパスで同じ場所を指しています。サーバー上のディレクトリを指定すると言っても結局はクライアントのディレクトリを指定しているのと変わりません。

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54918/abb14de7-e3a3-09ed-5755-8a9f0c2aa401.png)

### Windowsではどう見えるか

さてWindowsではどうなっているのでしょうか？WindowsではDockerを使う場合、一般的にDocker Desktop for Windowsを使います。実態はHyperV上のLinux仮想マシンです。このLinux仮想マシンにはホストOSであるWindows上のドライブが~~CIFS~~（[Docker Desktop Community 2.2.0.0](https://docs.docker.com/docker-for-windows/release-notes/#docker-desktop-community-2200) から）gRPC FUSE (grpcfuse)でマウントされています。マウント場所は`/host_mnt/c`ですが`/host_mnt/c`を指す`/c`というシンボリックリンクも作成されています。つまりLinux仮想マシンは`/c`で始まるパスでWindous上のファイルにアクセスできる仕組みになっています。

興味がある人のために、このLinux仮想マシン（DockerDesktopVM）の中身の調べ方を紹介します。

```sh
$ docker run -it --rm -v /:/vm alpine chroot /vm
```

DockerDesktopVM上の`/`ディレクトリをalpineコンテナの`/vm`に一旦マウントしてから、`/vm`を`/`に`chroot`しているだけです。ボリュームはサーバー上のディレクトリをマウントしているということがわかれば、なぜこれで中身が見れるのかもわかると思います。

さてそれではWindowsではどのようしてパスを処理しているのでしょうか？少し想像が入っていますが、DockerDesktopVMはWindows形式のパスを検出すると以下のようにLinuxのパスへの変換を行っているのだと思います。（WSL所のLinux版DockerクライアントでもWindows形式のパスが使えたのでクライアント側で変換してるのではないようです。）

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54918/eaa92282-8729-988a-4e84-2f8bcddd6c7c.png)

WSL上で使うのはLinux形式のパスなのでそのような変換は行われません。例えば`/mnt/c/Users/my-name/project`というパスはそのままサーバー上のパスとして扱われます。こんなパスはサーバー上にはないので、当然DockerサーバーはWindows上のファイルにはアクセス出来ません。

### 解決方法

#### よく見かける解決方法

DockerサーバーがWindows上のファイルにはアクセスできないのは要するにパスの不一致が原因なので合わせてやれば良いのです。その解決法の一つがWSLのWindows上のディレクトリの場所をデフォルトの`/mnt`ではなく`/`に変更する方法です。

```conf:/etc/wsl.conf
[automount]
root = /
options = "metadata"
```

こうすることでWSL上に`/c`以下にCドライブがマウントされ、クライアント側とサーバー側のパスが一致するので同じパスでDockerサーバーはWindows上のファイルを参照できるようになります。

#### 冒頭の解決方法

ところで`/mnt/c/Users/my-name/project`って長いですよね？私はよく使うディレクトリに素早くアクセスできるようにWSL上のホームディレクトリにシンボリックリンクを作成しています。例えば`/home/koichi/workspace`というシンボリックリンクのリンク先は`/mnt/c/Users/koichi/workspace`です。

さて、では`/home/koichi/workspace/project`にいるときに`docker run -it -v $PWD/data:/data debian`と実行したらどうなるでしょうか？カレントディレクトリ意味する`$PWD`の中身を展開すると`/home/koichi/workspace/project/data`となります。こんなディレクトリはDockerDesktopVM上にはありません。そのためWindows上のディレクトリにはアクセスできません。

docker-composeの場合は相対パスで指定できてシンボリックリンクは`/mnt/c/`以下のパスに解決されて渡されるようですが、dockerコマンドは絶対パスでしか指定ができずシンボリックリンクも解決されません。そこで冒頭の解決方法です。

（DockerDesktopVM上に）**パスがないなら作ればいいじゃない？**

もう分かったと思いますが、WSL側のパスを変えるのではなく、WSL上で使われているのと同じパスでアクセスできるようにDockerDesktopVMの中にシンボリックリンクを作っています。具体的にはWSL上で`/mnt/c`以下を参照するのでDockerDesktopVM上にも同じ`/mnt/c`という名前のシンボリックリンクを作成しています。このリンク先は`/c`です。（`/c`もシンボリックリンクなので最終的には`/host_mnt/c`につながります。）同様に私は必要なので`/home/koichi`というシンボリックリンクを作成して、`/c/Users/koichi`につなげています。

どのような使い方をしているかで作成すべきシンボリックリンクは変わりますが仕組みを理解すれば何を作成すれば良いかもわかると思います。

**この方法のデメリット**

まずDockerDesktopVMに作成したシンボリックリンクはDockerDesktopVMを再起動すると消えます。DockerDesktopVMに乗り込んでファイルシステムを見ればわかりますが`/var/lib`以外永続化されていません。そのためどこかの時点で再作成する必要があります。（例えば`.bashrc`で作成するなど。DockerDesktopVM再起動などのタイミングに仕込めれば良いのですが）

もう一つはDockerDesktopVMに変更を加えているところです。利用実績もないので何か気づいていない問題が発生するかもしれません。（例えばDocker Desktopのアップデート時など）なのでこの方法を使う場合は自己責任でお願いします。なにか問題があればコメントいただけると助かります。（実はWindows上でボリュームの扱いが面倒だったので、ボリュームを避けた使い方をしているので気づかない可能性が高いのです。）

あとこの方法によるデメリットというわけでは無いのですが、ボリュームのパスを間違えるとDockerDesktopVM上にディレクトリが作成されてしまいます。そしてそのディレクトリに後からシンボリックリンクを作成しようとするとエラーになりますので気をつけてください。普通はDockerDesktopVMを再起動すれば消えるはずですが。

以上、VOLUMEの解説とパス問題の解決法でした。