この情報は古く、現在のバージョンではソースコードが改変されているので、実情と異なる場合があります。
パスに:
を含むディレクトはマウント出来ない
Dockerコンテナにホストのディレクトリをマウントしようとしたら以下のようなエラーが発生しました。
% 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 でも発生する
% 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[:]: 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 はクローンしてきたリポジトリを管理してくれるツールで、 % ghq get URL
でリポジトリをクローンしてくれます。
% ghq get ssh://my.repo.io:12345/myrepo/backend.git
今回使っているリポジトリはポートを変えてホスティングしているためドメインにはポート番号が含まれています。
このリポジトリのフルパスを確認するとこのようになります。
% ghq list -p
/Users/masayuki/.ghq/my.repo.io:12345/myrepo/backend
このポート指定の :
が事の原因だったようです。
結論
- 回避する方法がない。
- コードにコミットしてプルリクを出すしか無い。
結論は回避方法がないということで、:
が含んでいると意図通りにマウント出来ないことがわかりました。
以下はその調査について書いていますので興味のある方はどうぞ。
dockerのソースコードを読む
:
のエスケープ方法を検索してもめぼしい情報が見つからなかったのでDockerのソースコードを調べてみることにしました。
DockerのRepositoryをcloneして該当箇所を探して確認します。
エラーメッセージの出力
闇雲にソースコードを読んでもしかたありません。
現状で有力な情報はエラーメッセージなので invalid bind mount spec
の出力箇所を探してみることにします。
# Docker Repositry のディレクトリで実行する
% grep -rin 'invalid bind mount spec' .
./runconfig/config.go:98: return fmt.Errorf("invalid bind mount spec %q: %v", spec, err)
どうやらここでエラーが出力されているようです。
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)
}
}
Dockerコマンドは Go言語 で書かれているんですね!!
-v
引数の解析
エラー出力箇所の前後をみると volume.ParseMountRaw()
の結果がエラーのようです。
ParseMountRaw
は命名からマウント引数の字句解析をしていそうです。次はこれを探してみます。
% 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
に定義されています。
func ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
arr, err := splitRawSpec(convertSlash(raw))
if err != nil {
return nil, err
}
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 onParseMountSpec
to
// validate the spec and create a MountPoint
要約すると ボリュームの仕様(例. -v /foo:/var:shared
)を解析しParseMountSpec
を使ってMountPoint
を作成します。 ということのようです。
ここの処理を理解すれば原因がわかりそうです。順に見てきす。
まずは splitRawSpec()
です。
% 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()
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
}
引数に :
が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)
で引数が :
で分割され配列に格納されています。
引き続き処理を見ていきましょう。
配列サイズによる分岐
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)
}
splitRawSpec()
の戻り値の配列サイズによって処理が分岐しています。
-v /Users/masayuki/.ghq/my.repo.io:12345/myrepo/backend:/var/www/backend
このように指定しているので配列サイズは3になるはずなので
// このように代入されるはずです
spec.Source = "/Users/masayuki/.ghq/my.repo.io"
spec.Target = "12345/myrepo/backend"
mode = "/var/www/backend"
mode のバリデーション
続いて mode
のバリデーションが行われます。ここで mode = "/var/www/backend"
が代入されているので ValidMountMode()
が通らずエラーが発生してしまいます。
if !ValidMountMode(mode) {
return nil, errInvalidMode(mode)
}
ValidMountMode()
を見ると仕様の通り ro,rw
などのモードのチェックを行っています。
https://github.com/moby/moby/blob/master/volume/volume_unix.go#L61-L96
つまり
ホストパスに :
が含まれているとそこで区切られてしまうので、意図通りの指定が出来ないことがわかります。
現状で出来る対策
- 絶対パスに
:
が含むものはマウントしない - ghq で clone しない
ソースを見る限り絶対パスに :
を含むディレクトリはマウント出来ないので、それを避けるしかありません。ファイルも同様です。
次に出来ることはプルリクを出すことなので、それを目指します。おしまい。