docker run -v のホストパスの制限について調べた


**この情報は古く、現在のバージョンではソースコードが改変されているので、実情と異なる場合があります。**


# パスに`:`を含むディレクトはマウント出来ない

Dockerコンテナにホストのディレクトリをマウントしようとしたら以下のようなエラーが発生しました。

```bash
% docker run --rm --name phpfpm -it -p 8000:8000 -v $(pwd):/var/www/backend phpfpm /bin/bash
docker: Error response from daemon: invalid bind mount spec "/Users/masayuki/.ghq/my.repo.io:12345/myrepo/backend:/var/www/backend": invalid mode: /var/www/backend.
See 'docker run --help'.
```

`var/www/backend` が `mode` として不正だということのようです。

## Docker Compose でも発生する

```console
% docker-compose up --build
Building php
Step 1/2 : FROM php:5.6-fpm
 ---> 3458979c7744
Step 2/2 : RUN docker-php-ext-install mysqli
 ---> Using cache
 ---> f223a09ff0c9

Successfully built f223a09ff0c9
Successfully tagged backend_php:latest
backend_adminer_1 is up-to-date
Creating backend_php_1 ...
Creating mysql         ...
Creating backend_nginx_1 ... error
Creating mysql           ... error
Creating backend_php_1   ... error

ERROR: for mysql  Cannot create container for service db: invalid mode: /var/lib/mysql

ERROR: for backend_php_1  Cannot create container for service php: invalid mode: /var/www/html

ERROR: for nginx  Cannot create container for service nginx: invalid mode: /var/www/html

ERROR: for db  Cannot create container for service db: invalid mode: /var/lib/mysql

ERROR: for php  Cannot create container for service php: invalid mode: /var/www/html
ERROR: Encountered errors while bringing up the project.
```

`docker-compose.yml` の `volumes:` ディレクティブで指定していても同じ原因でエラーが発生うする。


## `-v` オプションの仕様

`-v` オプションの仕様についてドキュメントで確認します。
https://docs.docker.com/engine/reference/run/#volume-shared-filesystems

> -v, --volume=[host-src:]container-dest[:<options>]: Bind mount a volume.

こちらからもわかるように `-v <ホストのパス>:<コンテナのパス>:<オプション>` が最長の指定方法で、区切り文字に `:` が使われています。
例えば `-v /home/user/project/htdocs:/var/www/html:ro` と指定すると、
ホストの `/home/user/project/htdocs` がコンテナの `/var/www/html` にReadOnlyでマウントされます。

エラーメッセージの `mode` とは `<options>` の指定部分の事のようです。



## ghq を使ってリポジトリを管理していた

ホストのディレクトリは　`ghq` で管理しているリポジトリのディレクトリでした。
[ghq](https://github.com/motemen/ghq) はクローンしてきたリポジトリを管理してくれるツールで、 `% ghq get URL` でリポジトリをクローンしてくれます。

```bash
% ghq get ssh://my.repo.io:12345/myrepo/backend.git
```

今回使っているリポジトリはポートを変えてホスティングしているためドメインにはポート番号が含まれています。
このリポジトリのフルパスを確認するとこのようになります。

```bash
% ghq list -p 
/Users/masayuki/.ghq/my.repo.io:12345/myrepo/backend
```

このポート指定の `:` が事の原因だったようです。

# 結論

- 回避する方法がない。
- コードにコミットしてプルリクを出すしか無い。

結論は回避方法がないということで、`:` が含んでいると意図通りにマウント出来ないことがわかりました。
以下はその調査について書いていますので興味のある方はどうぞ。


# dockerのソースコードを読む

`:` のエスケープ方法を検索してもめぼしい情報が見つからなかったのでDockerのソースコードを調べてみることにしました。
[DockerのRepository](https://github.com/moby/moby)をcloneして該当箇所を探して確認します。

## エラーメッセージの出力

闇雲にソースコードを読んでもしかたありません。
現状で有力な情報はエラーメッセージなので `invalid bind mount spec` の出力箇所を探してみることにします。


```bash
# Docker Repositry のディレクトリで実行する
% grep -rin 'invalid bind mount spec' .
./runconfig/config.go:98:			return fmt.Errorf("invalid bind mount spec %q: %v", spec, err)
```

どうやらここでエラーが出力されているようです。

```go:runconfig/config.go
	for _, spec := range hc.Binds {
		if _, err := volume.ParseMountRaw(spec, hc.VolumeDriver); err != nil {
			return fmt.Errorf("invalid bind mount spec %q: %v", spec, err)
		}
	}
```
https://github.com/moby/moby/blob/master/runconfig/config.go#L97-L99


Dockerコマンドは **Go言語** で書かれているんですね！！

## `-v` 引数の解析

エラー出力箇所の前後をみると `volume.ParseMountRaw()` の結果がエラーのようです。
`ParseMountRaw` は命名からマウント引数の字句解析をしていそうです。次はこれを探してみます。

```bash
% grep -rin 'ParseMountRaw' .
# 結果を抜粋
./volume/volume.go:209:// ParseMountRaw parses a raw volume spec (e.g. `-v /foo:/bar:shared`) into a
./volume/volume.go:212:func ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
./volume/volume_test.go:154:func TestParseMountRawSplit(t *testing.T) {

```

`volumne.go` に定義されています。

```go:volume/volume.go
func ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
	arr, err := splitRawSpec(convertSlash(raw))
	if err != nil {
		return nil, err
	}

```
https://github.com/moby/moby/blob/master/volume/volume.go#L212-L217


## `ParseMountRaw()` の処理内容

関数のコメントにはこのように書かれています。

> // ParseMountRaw parses a raw volume spec (e.g. `-v /foo:/bar:shared`) into a
// structured spec. Once the raw spec is parsed it relies on `ParseMountSpec` to
// validate the spec and create a MountPoint

要約すると **ボリュームの仕様（例. `-v /foo:/var:shared`）を解析し`ParseMountSpec`を使って`MountPoint`を作成します。**　ということのようです。

ここの処理を理解すれば原因がわかりそうです。順に見てきす。
まずは `splitRawSpec()` です。

```bash
% grep -rin 'splitRawSpec' volume
volume/volume.go:213:	arr, err := splitRawSpec(convertSlash(raw))
volume/volume_unix.go:130:func splitRawSpec(raw string) ([]string, error) {
volume/volume_windows.go:93:fun

```

`volume/volume_unix.go` を開きます。

## `splitRawSpec()`


```go#volume/volume_unix.go
func splitRawSpec(raw string) ([]string, error) {
	if strings.Count(raw, ":") > 2 {
		return nil, errInvalidSpec(raw)
	}

	arr := strings.SplitN(raw, ":", 3)
	if arr[0] == "" {
		return nil, errInvalidSpec(raw)
	}
	return arr, nil
}

```
https://github.com/moby/moby/blob/master/volume/volume_unix.go#L130-L140


引数に `:` が2を超えて含まれる時、エラーが発生していることがわかります。
つまり ` -v /some.domain.io:8080/foo:/bar/:shared` という指定が出来ないことがわかります。

今回のエラーは `-v /Users/masayuki/.ghq/my.repo.io:12345/myrepo/backend:/var/www/backend` の指定で `:` は2つなのでここでのエラーが原因ではありません。

次の `strings.SplitN(raw, ":", 3)` で引数が `:` で分割され配列に格納されています。

引き続き処理を見ていきましょう。


## 配列サイズによる分岐

```go#volume/volume.go
	var spec mounttypes.Mount
	var mode string
	switch len(arr) {
	case 1:
		// Just a destination path in the container
		spec.Target = arr[0]
	case 2:
		if ValidMountMode(arr[1]) {
			// Destination + Mode is not a valid volume - volumes
			// cannot include a mode. e.g. /foo:rw
			return nil, errInvalidSpec(raw)
		}
		// Host Source Path or Name + Destination
		spec.Source = arr[0]
		spec.Target = arr[1]
	case 3:
		// HostSourcePath+DestinationPath+Mode
		spec.Source = arr[0]
		spec.Target = arr[1]
		mode = arr[2]
	default:
		return nil, errInvalidSpec(raw)
	}
```
https://github.com/moby/moby/blob/master/volume/volume.go#L218-L240


`splitRawSpec()` の戻り値の配列サイズによって処理が分岐しています。
`-v /Users/masayuki/.ghq/my.repo.io:12345/myrepo/backend:/var/www/backend` このように指定しているので配列サイズは3になるはずなので

```go
// このように代入されるはずです
		spec.Source = "/Users/masayuki/.ghq/my.repo.io"
		spec.Target = "12345/myrepo/backend"
		mode = "/var/www/backend"
```


## mode のバリデーション

続いて　`mode` のバリデーションが行われます。ここで `mode = "/var/www/backend"` が代入されているので `ValidMountMode()` が通らずエラーが発生してしまいます。

```go
	if !ValidMountMode(mode) {
		return nil, errInvalidMode(mode)
	}
```
https://github.com/moby/moby/blob/master/volume/volume.go#L242-L244


`ValidMountMode()` を見ると仕様の通り `ro,rw` などのモードのチェックを行っています。
https://github.com/moby/moby/blob/master/volume/volume_unix.go#L61-L96

## つまり

ホストパスに `:` が含まれているとそこで区切られてしまうので、意図通りの指定が出来ないことがわかります。


# 現状で出来る対策

- 絶対パスに `:` が含むものはマウントしない
- ghq で clone しない

ソースを見る限り絶対パスに `:` を含むディレクトリはマウント出来ないので、それを避けるしかありません。ファイルも同様です。

次に出来ることはプルリクを出すことなので、それを目指します。おしまい。